53 Commits
v1b9 ... v1b13

Author SHA1 Message Date
Marcel Baumgartner
a295354549 Merge pull request #210 from Dannyx1604/patch-1
I got bored again (de_de.lang)
2023-07-07 03:23:31 +02:00
Marcel Baumgartner
749ea5dc8e Merge pull request #211 from Moonlight-Panel/AddTelemetryReporter
Added telemetry reporter
2023-07-07 03:14:06 +02:00
Marcel Baumgartner
f52b9e2951 Added telemetry reporter 2023-07-07 03:06:16 +02:00
Dannyx
d2dbb68967 I got bored again (de_de.lang) 2023-07-06 23:28:08 +02:00
Marcel Baumgartner
d1c9009e9f Merge pull request #209 from Moonlight-Panel/NewVisualConfigEditor
Added a new visual config editor
2023-07-06 16:46:29 +02:00
Marcel Baumgartner
d024a834f9 Added a new visual config editor 2023-07-06 16:46:01 +02:00
Marcel Baumgartner
ab529991fd Fix some javascript not loaded issues
Because all js files are executed in the order they were put into the document some js files were not loaded while starting blazor. this should fix it (hopefully ;) )
2023-07-04 18:06:14 +02:00
Marcel Baumgartner
92705837ba Merge pull request #208 from Moonlight-Panel/RewriteSessionSystem
Rewritten session system to match new standarts and be more performant
2023-07-04 17:51:09 +02:00
Marcel Baumgartner
609d5451f9 Rewritten session system to match new standarts and be more performant 2023-07-04 17:49:27 +02:00
Marcel Baumgartner
2bb2caeeed Merge pull request #207 from Moonlight-Panel/AddIpLogs
Added ip log for register and last visit
2023-07-03 20:17:20 +02:00
Marcel Baumgartner
61db49bfb7 Added ip log for register and last visit 2023-07-03 20:01:34 +02:00
Marcel Baumgartner
a75678d305 Merge pull request #206 from Moonlight-Panel/SmallFixes
Small fixes
2023-07-02 21:33:12 +02:00
Marcel Baumgartner
d418c91efa Fixed js invoke errors 2023-07-02 21:30:34 +02:00
Marcel Baumgartner
7f2da5a55d Updated sweet alert 2 2023-07-02 20:56:10 +02:00
Marcel Baumgartner
5e592ccdcb Added ignore for unexpected dispose errors 2023-07-02 20:51:08 +02:00
Marcel Baumgartner
016f50fb1c Added ignore for json serialize errors 2023-07-02 20:48:47 +02:00
Marcel Baumgartner
fe21668a2b Removed wrong logged warn 2023-07-02 20:44:29 +02:00
Marcel Baumgartner
1aab86a317 Fixed wrong ssl config for nodes 2023-07-02 20:41:31 +02:00
Marcel Baumgartner
243d23d4e2 Fixed repatcha config for empty values 2023-07-02 20:40:47 +02:00
Marcel Baumgartner
2fe17473ae Merge pull request #204 from Moonlight-Panel/SwitchToNewConfigSystem
Switched to new config system
2023-07-02 02:19:32 +02:00
Marcel Baumgartner
609cf8cfac Switched to new config system 2023-07-02 02:16:44 +02:00
Marcel Baumgartner
678da30b09 Merge pull request #203 from Moonlight-Panel/LogsAndFixes
Added new log files and log migrators. Fixed some errors with js invokes
2023-07-02 00:21:55 +02:00
Marcel Baumgartner
d19412f4bb Added new log files and log migrators. Fixed some errors with js invokes 2023-07-02 00:21:35 +02:00
Marcel Baumgartner
1665d6e537 Merge pull request #202 from Moonlight-Panel/AddSentrySupport
Added new sentry support
2023-07-01 19:01:00 +02:00
Marcel Baumgartner
fd210f2404 Added new sentry support 2023-07-01 19:00:38 +02:00
Marcel Baumgartner
c33729fb44 Merge pull request #201 from Moonlight-Panel/FixServerList
Fixed server list
2023-06-30 21:55:58 +02:00
Marcel Baumgartner
7983bf3ee4 Fixed server list 2023-06-30 21:55:32 +02:00
Marcel Baumgartner
a09f60aea7 Merge pull request #200 from Moonlight-Panel/HttpTimeoutFixes
Fixed timeout options from assumed seconds to real value miliseconds
2023-06-29 23:28:07 +02:00
Marcel Baumgartner
28b5893c21 Fixed timeout options from assumed seconds to real value miliseconds 2023-06-29 23:27:48 +02:00
Marcel Baumgartner
25b47d8b6c Merge pull request #199 from Moonlight-Panel/AddNewGermanTranslation
Added new german translations
2023-06-29 23:10:21 +02:00
Marcel Baumgartner
85f5b8a7da Added new german translations 2023-06-29 23:09:58 +02:00
Marcel Baumgartner
ab9333f99a Merge pull request #198 from Moonlight-Panel/FixServerDelete
Fixed server delete
2023-06-26 22:51:17 +02:00
Marcel Baumgartner
d60f8fc905 Fixed server delete 2023-06-26 22:50:45 +02:00
Marcel Baumgartner
fe1f4412d8 Merge pull request #197 from Moonlight-Panel/AddedRetry
Added retry class and added it to some api calls that need it
2023-06-26 22:45:56 +02:00
Marcel Baumgartner
f191533410 Added retry class and added it to some api calls that need it 2023-06-26 22:45:33 +02:00
Marcel Baumgartner
a894707536 Merge pull request #196 from Moonlight-Panel/SmallBugFixes
Small bug fixes
2023-06-26 18:05:58 +02:00
Marcel Baumgartner
d2ccc84286 Fixed sidebar text sizing issues 2023-06-26 17:23:25 +02:00
Marcel Baumgartner
f2ec43f2d2 Fixed not enabled installing screen for new servers 2023-06-26 17:19:54 +02:00
Marcel Baumgartner
7feccc8d9f Fixed error loop for fileaccess providers not supporting the launch url 2023-06-26 17:19:32 +02:00
Marcel Baumgartner
a8bd1193ce Merge pull request #195 from Moonlight-Panel/SecurityPatches
Security patches
2023-06-26 00:10:03 +02:00
Marcel Baumgartner
366d1a9205 Merge pull request #194 from Moonlight-Panel/AddStreamerModeAndFixUI
Added streamer mode and fixed security settings ui
2023-06-26 00:07:23 +02:00
Marcel Baumgartner
df9ed95c6b Added streamer mode and fixed security settings ui 2023-06-26 00:06:44 +02:00
Marcel Baumgartner
23a211362e Merge pull request #193 from Moonlight-Panel/EnhanceFileManager
Enhanced winscp button and new file/folder menu
2023-06-25 17:32:57 +02:00
Marcel Baumgartner
cf91d44902 Enhanced winscp button and new file/folder menu 2023-06-25 17:31:36 +02:00
Marcel Baumgartner
35633e21a9 Merge pull request #192 from Moonlight-Panel/EnhanceServerListLayout
Enhanced server list
2023-06-25 00:01:53 +02:00
Marcel Baumgartner
ce8b8f6798 Enhanced server list 2023-06-25 00:01:28 +02:00
Marcel Baumgartner
c28c80ba25 Merge pull request #191 from Moonlight-Panel/FixDnsManager
Fixed dns loading issues. Added udp support
2023-06-24 23:45:49 +02:00
Marcel Baumgartner
da17b1df93 Fixed dns loading issues. Added udp support 2023-06-24 23:45:29 +02:00
Marcel Baumgartner
f9f5865ef9 Prevent user locking when duplicating the email entries 2023-06-24 22:35:38 +02:00
Marcel Baumgartner
389ded9b77 Fixed oauth2 account spoofing using unverified discord accounts for claiming identity 2023-06-24 22:15:04 +02:00
Marcel Baumgartner
faebaa59dd Merge pull request #189 from Moonlight-Panel/AddCustomLayoutServerList
Added custom layout options for the server list
2023-06-24 02:35:21 +02:00
Marcel Baumgartner
6b7dc2ad05 Added custom layout options for the server list 2023-06-24 02:35:01 +02:00
Marcel Baumgartner
e356c9d0c8 Update README.md 2023-06-23 05:06:12 +02:00
99 changed files with 6385 additions and 1700 deletions

View File

@@ -47,7 +47,7 @@ public class ModrinthApiHelper
var request = new RestRequest(url)
{
Timeout = 60 * 15
Timeout = 300000
};
request.AddHeader("Content-Type", "application/json");

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

@@ -209,7 +209,7 @@ public class WingsApiHelper
var request = new RestRequest(url)
{
Timeout = 60 * 15
Timeout = 300000
};
request.AddHeader("Content-Type", "application/json");

View File

@@ -0,0 +1,320 @@
using System.ComponentModel;
using Moonlight.App.Helpers;
namespace Moonlight.App.Configuration;
using System;
using Newtonsoft.Json;
public class ConfigV1
{
[JsonProperty("Moonlight")]
public MoonlightData Moonlight { get; set; } = new();
public class MoonlightData
{
[JsonProperty("AppUrl")]
[Description("The url moonlight is accesible with from the internet")]
public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash";
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
[JsonProperty("DiscordBotApi")] public DiscordBotApiData DiscordBotApi { get; set; } = new();
[JsonProperty("DiscordBot")] public DiscordBotData DiscordBot { get; set; } = new();
[JsonProperty("Domains")] public DomainsData Domains { get; set; } = new();
[JsonProperty("Html")] public HtmlData Html { get; set; } = new();
[JsonProperty("Marketing")] public MarketingData Marketing { get; set; } = new();
[JsonProperty("OAuth2")] public OAuth2Data OAuth2 { get; set; } = new();
[JsonProperty("Security")] public SecurityData Security { get; set; } = new();
[JsonProperty("Mail")] public MailData Mail { get; set; } = new();
[JsonProperty("Cleanup")] public CleanupData Cleanup { get; set; } = new();
[JsonProperty("Subscriptions")] public SubscriptionsData Subscriptions { get; set; } = new();
[JsonProperty("DiscordNotifications")]
public DiscordNotificationsData DiscordNotifications { get; set; } = new();
[JsonProperty("Statistics")] public StatisticsData Statistics { get; set; } = new();
[JsonProperty("Rating")] public RatingData Rating { get; set; } = new();
[JsonProperty("SmartDeploy")] public SmartDeployData SmartDeploy { get; set; } = new();
[JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new();
}
public class CleanupData
{
[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")]
[Description("The minumum amount of memory in megabytes avaliable before the cleanup starts")]
public long Memory { get; set; } = 8192;
[JsonProperty("Wait")]
[Description("The delay between every cleanup check in minutes")]
public long Wait { get; set; } = 15;
[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")]
[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")]
[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
{
[JsonProperty("Database")] public string Database { get; set; } = "moonlight_db";
[JsonProperty("Host")] public string Host { get; set; } = "your.database.host";
[JsonProperty("Password")]
[Blur]
public string Password { get; set; } = "secret";
[JsonProperty("Port")] public long Port { get; set; } = 3306;
[JsonProperty("Username")] public string Username { get; set; } = "moonlight_user";
}
public class DiscordBotApiData
{
[JsonProperty("Enable")]
[Description("Enable the discord bot api. Currently only DatBot is using this api")]
public bool Enable { get; set; } = false;
[JsonProperty("Token")]
[Description("Specify the token the api client needs to provide")]
[Blur]
public string Token { get; set; } = Guid.NewGuid().ToString();
}
public class DiscordBotData
{
[JsonProperty("Enable")]
[Description("The discord bot can be used to allow customers to manage their servers via discord")]
public bool Enable { get; set; } = false;
[JsonProperty("Token")]
[Description("Your discord bot token goes here")]
[Blur]
public string Token { get; set; } = "discord token here";
[JsonProperty("PowerActions")]
[Description("Enable actions like starting and stopping servers")]
public bool PowerActions { get; set; } = false;
[JsonProperty("SendCommands")]
[Description("Allow users to send commands to their servers")]
public bool SendCommands { get; set; } = false;
}
public class DiscordNotificationsData
{
[JsonProperty("Enable")]
[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")]
[Description("The discord webhook the notifications are being sent to")]
[Blur]
public string WebHook { get; set; } = "http://your-discord-webhook-url";
}
public class DomainsData
{
[JsonProperty("Enable")]
[Description("This enables the domain system")]
public bool Enable { get; set; } = false;
[JsonProperty("AccountId")]
[Description("This option specifies the cloudflare account id")]
public string AccountId { get; set; } = "cloudflare acc id";
[JsonProperty("Email")]
[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
{
[JsonProperty("Headers")] public HeadersData Headers { get; set; } = new();
}
public class HeadersData
{
[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")]
[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")]
[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")]
[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
{
[JsonProperty("Email")] public string Email { get; set; } = "username@your.mail.host";
[JsonProperty("Server")] public string Server { get; set; } = "your.mail.host";
[JsonProperty("Password")]
[Blur]
public string Password { get; set; } = "secret";
[JsonProperty("Port")] public int Port { get; set; } = 465;
[JsonProperty("Ssl")] public bool Ssl { get; set; } = true;
}
public class MarketingData
{
[JsonProperty("BrandName")] public string BrandName { get; set; } = "Endelon Hosting";
[JsonProperty("Imprint")] public string Imprint { get; set; } = "https://your-site.xyz/imprint";
[JsonProperty("Privacy")] public string Privacy { get; set; } = "https://your-site.xyz/privacy";
[JsonProperty("About")] public string About { get; set; } = "https://your-site.xyz/about";
[JsonProperty("Website")] public string Website { get; set; } = "https://your-site.xyz";
}
public class OAuth2Data
{
[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")]
[Description("This enables the url override")]
public bool EnableOverrideUrl { get; set; } = false;
[JsonProperty("Providers")]
public OAuth2ProviderData[] Providers { get; set; } = Array.Empty<OAuth2ProviderData>();
}
public class OAuth2ProviderData
{
[JsonProperty("Id")] public string Id { get; set; }
[JsonProperty("ClientId")] public string ClientId { get; set; }
[JsonProperty("ClientSecret")]
[Blur]
public string ClientSecret { get; set; }
}
public class RatingData
{
[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")]
[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")]
[Description("The minimum star count on the rating ranging from 1 to 5")]
public int MinRating { get; set; } = 4;
[JsonProperty("DaysSince")]
[Description("The days a user has to be registered to even be able to get this popup")]
public int DaysSince { get; set; } = 5;
}
public class SecurityData
{
[JsonProperty("Token")]
[Description("This is the moonlight app token. It is used to encrypt and decrypt data and validte tokens and sessions")]
[Blur]
public string Token { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
}
public class ReCaptchaData
{
[JsonProperty("Enable")]
[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")]
[Blur]
public string SiteKey { get; set; } = "recaptcha site key here";
[JsonProperty("SecretKey")]
[Blur]
public string SecretKey { get; set; } = "recaptcha secret here";
}
public class SentryData
{
[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")]
[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
{
[JsonProperty("Server")] public SmartDeployServerData Server { get; set; } = new();
}
public class SmartDeployServerData
{
[JsonProperty("EnableOverride")] public bool EnableOverride { get; set; } = false;
[JsonProperty("OverrideNode")] public long OverrideNode { get; set; } = 1;
}
public class StatisticsData
{
[JsonProperty("Enabled")] public bool Enabled { get; set; } = false;
[JsonProperty("Wait")] public long Wait { get; set; } = 15;
}
public class SubscriptionsData
{
[JsonProperty("SellPass")] public SellPassData SellPass { get; set; } = new();
}
public class SellPassData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Url")] public string Url { get; set; } = "https://not-implemented-yet";
}
}

View File

@@ -52,14 +52,14 @@ public class DataContext : DbContext
if (!optionsBuilder.IsConfigured)
{
var config = ConfigService
.GetSection("Moonlight")
.GetSection("Database");
.Get()
.Moonlight.Database;
var connectionString = $"host={config.GetValue<string>("Host")};" +
$"port={config.GetValue<int>("Port")};" +
$"database={config.GetValue<string>("Database")};" +
$"uid={config.GetValue<string>("Username")};" +
$"pwd={config.GetValue<string>("Password")}";
var connectionString = $"host={config.Host};" +
$"port={config.Port};" +
$"database={config.Database};" +
$"uid={config.Username};" +
$"pwd={config.Password}";
optionsBuilder.UseMySql(
connectionString,

View File

@@ -24,6 +24,8 @@ public class User
public string Country { get; set; } = "";
public string ServerListLayoutJson { get; set; } = "";
// States
public UserStatus Status { get; set; } = UserStatus.Unverified;
@@ -31,6 +33,7 @@ public class User
public bool SupportPending { get; set; } = false;
public bool HasRated { get; set; } = false;
public int Rating { get; set; } = 0;
public bool StreamerMode { get; set; } = false;
// Security
public bool TotpEnabled { get; set; } = false;
@@ -50,4 +53,8 @@ public class User
public Subscription? CurrentSubscription { get; set; } = null;
public DateTime SubscriptionSince { get; set; } = DateTime.Now;
public int SubscriptionDuration { get; set; }
// Ip logs
public string RegisterIp { get; set; } = "";
public string LastIp { get; set; } = "";
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedServerListLayoutToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ServerListLayoutJson",
table: "Users",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ServerListLayoutJson",
table: "Users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedStreamerMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "StreamerMode",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "StreamerMode",
table: "Users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedIpLogsForUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "LastIp",
table: "Users",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "RegisterIp",
table: "Users",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastIp",
table: "Users");
migrationBuilder.DropColumn(
name: "RegisterIp",
table: "Users");
}
}
}

View File

@@ -766,6 +766,10 @@ namespace Moonlight.App.Database.Migrations
b.Property<bool>("HasRated")
.HasColumnType("tinyint(1)");
b.Property<string>("LastIp")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("longtext");
@@ -780,6 +784,14 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Rating")
.HasColumnType("int");
b.Property<string>("RegisterIp")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ServerListLayoutJson")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("State")
.IsRequired()
.HasColumnType("longtext");
@@ -787,6 +799,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Status")
.HasColumnType("int");
b.Property<bool>("StreamerMode")
.HasColumnType("tinyint(1)");
b.Property<int>("SubscriptionDuration")
.HasColumnType("int");

View File

@@ -0,0 +1,30 @@
using Microsoft.JSInterop;
namespace Moonlight.App.Extensions;
public static class JSRuntimeExtensions
{
public static async Task InvokeVoidSafeAsync(this IJSRuntime jsRuntime, string method, params object[] args)
{
try
{
await jsRuntime.InvokeVoidAsync(method, args);
}
catch (Exception)
{
// ignored
}
}
public static void InvokeVoidSafe(this IJSRuntime jsRuntime, string method, params object[] args)
{
try
{
jsRuntime.InvokeVoidAsync(method, args);
}
catch (Exception)
{
// ignored
}
}
}

View File

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

View File

@@ -66,15 +66,13 @@ public class DatabaseCheckupService
var configService = new ConfigService(new StorageService());
var dateTimeService = new DateTimeService();
var config = configService
.GetSection("Moonlight")
.GetSection("Database");
var config = configService.Get().Moonlight.Database;
var connectionString = $"host={config.GetValue<string>("Host")};" +
$"port={config.GetValue<int>("Port")};" +
$"database={config.GetValue<string>("Database")};" +
$"uid={config.GetValue<string>("Username")};" +
$"pwd={config.GetValue<string>("Password")}";
var connectionString = $"host={config.Host};" +
$"port={config.Port};" +
$"database={config.Database};" +
$"uid={config.Username};" +
$"pwd={config.Password}";
string file = PathBuilder.File("storage", "backups", $"{dateTimeService.GetCurrentUnix()}-mysql.sql");

View File

@@ -111,7 +111,7 @@ public class WingsFileAccess : FileAccess
request.AddParameter("name", "files");
request.AddParameter("filename", name);
request.AddHeader("Content-Type", "multipart/form-data");
request.AddHeader("Origin", ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl"));
request.AddHeader("Origin", ConfigService.Get().Moonlight.AppUrl);
request.AddFile("files", () =>
{
return new StreamProgressHelper(dataStream)

View File

@@ -1,9 +1,36 @@
using Moonlight.App.Services;
using System.Text;
using Moonlight.App.Services;
namespace Moonlight.App.Helpers;
public static class Formatter
{
public static string ReplaceEnd(string input, string substringToReplace, string newSubstring)
{
int lastIndexOfSubstring = input.LastIndexOf(substringToReplace);
if (lastIndexOfSubstring >= 0)
{
input = input.Remove(lastIndexOfSubstring, substringToReplace.Length).Insert(lastIndexOfSubstring, newSubstring);
}
return input;
}
public static string ConvertCamelCaseToSpaces(string input)
{
StringBuilder output = new StringBuilder();
foreach (char c in input)
{
if (char.IsUpper(c))
{
output.Append(' ');
}
output.Append(c);
}
return output.ToString().Trim();
}
public static string FormatUptime(double uptime)
{
TimeSpan t = TimeSpan.FromMilliseconds(uptime);

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

@@ -0,0 +1,66 @@
namespace Moonlight.App.Helpers;
public class Retry
{
private List<Type> RetryExceptionTypes;
private List<Func<Exception, bool>> RetryFilters;
private int RetryTimes = 1;
public Retry()
{
RetryExceptionTypes = new();
RetryFilters = new();
}
public Retry Times(int times)
{
RetryTimes = times;
return this;
}
public Retry At(Func<Exception, bool> filter)
{
RetryFilters.Add(filter);
return this;
}
public Retry At<T>()
{
RetryExceptionTypes.Add(typeof(T));
return this;
}
public async Task Call(Func<Task> method)
{
int triesLeft = RetryTimes;
do
{
try
{
await method.Invoke();
return;
}
catch (Exception e)
{
if(triesLeft < 1) // Throw if no tries left
throw;
if (RetryExceptionTypes.Any(x => x.FullName == e.GetType().FullName))
{
triesLeft--;
continue;
}
if (RetryFilters.Any(x => x.Invoke(e)))
{
triesLeft--;
continue;
}
// Throw if not filtered -> unknown/unhandled
throw;
}
} while (triesLeft >= 0);
}
}

View File

@@ -243,6 +243,7 @@ public class WingsConsole : IDisposable
}
}
catch(JsonReaderException){}
catch(JsonSerializationException){}
catch (Exception e)
{
if (!Disconnecting)

View File

@@ -20,7 +20,7 @@ public class WingsConsoleHelper
{
ServerRepository = serverRepository;
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
AppUrl = configService.Get().Moonlight.AppUrl;
}
public async Task ConnectWings(WingsConsole console, Server server)

View File

@@ -15,7 +15,7 @@ public class WingsJwtHelper
{
ConfigService = configService;
AppUrl = ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl");
AppUrl = ConfigService.Get().Moonlight.AppUrl;
}
public string Generate(string secret, Action<Dictionary<string, string>> claimsAction)

View File

@@ -30,14 +30,14 @@ public class DiscordBotController : Controller
ServerService = serverService;
var config = configService
.GetSection("Moonlight")
.GetSection("DiscordBotApi");
.Get()
.Moonlight.DiscordBotApi;
Enable = config.GetValue<bool>("Enable");
Enable = config.Enable;
if (Enable)
{
Token = config.GetValue<string>("Token");
Token = config.Token;
}
}

View File

@@ -27,19 +27,39 @@ public class LogMigrator : ILogger
switch (logLevel)
{
case LogLevel.Critical:
Logger.Fatal($"[{Name}] {formatter(state, exception)}");
Logger.Fatal(formatter(state, exception));
if(exception != null)
Logger.Fatal(exception);
break;
case LogLevel.Warning:
Logger.Warn($"[{Name}] {formatter(state, exception)}");
Logger.Warn(formatter(state, exception));
if(exception != null)
Logger.Warn(exception);
break;
case LogLevel.Debug:
Logger.Debug($"[{Name}] {formatter(state, exception)}");
Logger.Debug(formatter(state, exception));
if(exception != null)
Logger.Debug(exception);
break;
case LogLevel.Error:
Logger.Error($"[{Name}] {formatter(state, exception)}");
Logger.Error(formatter(state, exception));
if(exception != null)
Logger.Error(exception);
break;
case LogLevel.Information:
Logger.Info($"[{Name}] {formatter(state, exception)}");
Logger.Info(formatter(state, exception));
if(exception != null)
Logger.Info(exception);
break;
}
}

View File

@@ -0,0 +1,71 @@
using Moonlight.App.Helpers;
using Sentry;
using Sentry.Extensibility;
namespace Moonlight.App.LogMigrator;
public class SentryDiagnosticsLogger : IDiagnosticLogger
{
private readonly SentryLevel Level;
public SentryDiagnosticsLogger(SentryLevel level)
{
Level = level;
}
public bool IsEnabled(SentryLevel level)
{
if ((int)level >= (int)Level)
{
return true;
}
return false;
}
public void Log(SentryLevel logLevel, string message, Exception? exception = null, params object?[] args)
{
switch (logLevel)
{
case SentryLevel.Debug:
Logger.Debug(string.Format(message, args));
if(exception != null)
Logger.Debug(exception);
break;
case SentryLevel.Info:
Logger.Info(string.Format(message, args));
if(exception != null)
Logger.Info(exception);
break;
case SentryLevel.Warning:
Logger.Warn(string.Format(message, args));
if(exception != null)
Logger.Warn(exception);
break;
case SentryLevel.Error:
Logger.Error(string.Format(message, args));
if(exception != null)
Logger.Error(exception);
break;
case SentryLevel.Fatal:
Logger.Fatal(string.Format(message, args));
if(exception != null)
Logger.Fatal(exception);
break;
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.App.Models.Forms;
public class UserPreferencesDataModel
{
public bool StreamerMode { get; set; } = false;
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.App.Models.Misc;
public class ServerGroup
{
public string Name { get; set; } = "";
public List<string> Servers { get; set; } = new();
}

View File

@@ -1,16 +0,0 @@
using Microsoft.AspNetCore.Components;
using Moonlight.App.Database.Entities;
using Moonlight.App.Services.Interop;
namespace Moonlight.App.Models.Misc;
public class Session
{
public string Ip { get; set; } = "N/A";
public string Url { get; set; } = "N/A";
public string Device { get; set; } = "N/A";
public User? User { get; set; }
public DateTime CreatedAt { get; set; }
public NavigationManager Navigation { get; set; }
public AlertService AlertService { get; set; }
}

View File

@@ -86,6 +86,13 @@ public class DiscordOAuth2Provider : OAuth2Provider
var email = getData.GetValue<string>("email");
var id = getData.GetValue<ulong>("id");
var verified = getData.GetValue<bool>("verified");
if (!verified)
{
Logger.Warn("A user tried to use an unverified discord account to login", "security");
throw new DisplayException("You can only use verified discord accounts for oauth signin");
}
// Handle data

View File

@@ -1,37 +0,0 @@
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Repositories;
public class SessionRepository
{
private readonly List<Session> Sessions;
public SessionRepository()
{
Sessions = new();
}
public Session[] Get()
{
lock (Sessions)
{
return Sessions.ToArray();
}
}
public void Add(Session session)
{
lock (Sessions)
{
Sessions.Add(session);
}
}
public void Delete(Session session)
{
lock (Sessions)
{
Sessions.Remove(session);
}
}
}

View File

@@ -43,15 +43,15 @@ public class CleanupService
CompletedAt = DateTimeService.GetCurrent();
IsRunning = false;
var config = ConfigService.GetSection("Moonlight").GetSection("Cleanup");
var config = ConfigService.Get().Moonlight.Cleanup;
if (!config.GetValue<bool>("Enable") || ConfigService.DebugMode)
if (!config.Enable || ConfigService.DebugMode)
{
Logger.Info("Disabling cleanup service");
return;
}
Timer = new(TimeSpan.FromMinutes(config.GetValue<int>("Wait")));
Timer = new(TimeSpan.FromMinutes(config.Wait));
Task.Run(Run);
}
@@ -63,12 +63,12 @@ public class CleanupService
IsRunning = true;
using var scope = ServiceScopeFactory.CreateScope();
var config = ConfigService.GetSection("Moonlight").GetSection("Cleanup");
var config = ConfigService.Get().Moonlight.Cleanup;
var maxCpu = config.GetValue<int>("Cpu");
var minMemory = config.GetValue<int>("Memory");
var maxUptime = config.GetValue<int>("Uptime");
var minUptime = config.GetValue<int>("MinUptime");
var maxCpu = config.Cpu;
var minMemory = config.Memory;
var maxUptime = config.Uptime;
var minUptime = config.MinUptime;
var nodeRepository = scope.ServiceProvider.GetRequiredService<NodeRepository>();
var nodeService = scope.ServiceProvider.GetRequiredService<NodeService>();

View File

@@ -22,14 +22,14 @@ public class DiscordNotificationService
Event = eventSystem;
ResourceService = resourceService;
var config = configService.GetSection("Moonlight").GetSection("DiscordNotifications");
var config = configService.Get().Moonlight.DiscordNotifications;
if (config.GetValue<bool>("Enable"))
if (config.Enable)
{
Logger.Info("Discord notifications enabled");
Client = new(config.GetValue<string>("WebHook"));
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
Client = new(config.WebHook);
AppUrl = configService.Get().Moonlight.AppUrl;
Event.On<User>("supportChat.new", this, OnNewSupportChat);
Event.On<SupportChatMessage>("supportChat.message", this, OnSupportChatMessage);

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

@@ -1,15 +1,14 @@
using System.Text;
using Microsoft.Extensions.Primitives;
using Moonlight.App.Configuration;
using Moonlight.App.Helpers;
using Moonlight.App.Services.Files;
using Newtonsoft.Json;
namespace Moonlight.App.Services;
public class ConfigService : IConfiguration
public class ConfigService
{
private readonly StorageService StorageService;
private IConfiguration Configuration;
private ConfigV1 Configuration;
public bool DebugMode { get; private set; } = false;
public bool SqlDebugMode { get; private set; } = false;
@@ -41,33 +40,42 @@ public class ConfigService : IConfiguration
public void Reload()
{
Configuration = new ConfigurationBuilder().AddJsonStream(
new MemoryStream(Encoding.ASCII.GetBytes(
File.ReadAllText(
PathBuilder.File("storage", "configs", "config.json")
)
)
)).Build();
var path = PathBuilder.File("storage", "configs", "config.json");
if (!File.Exists(path))
{
File.WriteAllText(path, "{}");
}
public IEnumerable<IConfigurationSection> GetChildren()
{
return Configuration.GetChildren();
Configuration = JsonConvert.DeserializeObject<ConfigV1>(
File.ReadAllText(path)
) ?? new ConfigV1();
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented));
}
public IChangeToken GetReloadToken()
public void Save(ConfigV1 configV1)
{
return Configuration.GetReloadToken();
Configuration = configV1;
Save();
}
public IConfigurationSection GetSection(string key)
public void Save()
{
return Configuration.GetSection(key);
var path = PathBuilder.File("storage", "configs", "config.json");
if (!File.Exists(path))
{
File.WriteAllText(path, "{}");
}
public string this[string key]
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented));
Reload();
}
public ConfigV1 Get()
{
get => Configuration[key];
set => Configuration[key] = value;
return Configuration;
}
}

View File

@@ -29,7 +29,7 @@ public class ServerListCommand : BaseModule
{
embed = dcs.EmbedBuilderModule.StandardEmbed("Sorry ;( \n Please first create and/or link a Account to Discord! \n Press the Button to register/log in.", Color.Red, command.User);
components = new ComponentBuilder();
components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.GetSection("Moonlight").GetValue<String>("AppUrl"));
components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.Get().Moonlight.AppUrl);
await command.RespondAsync(embed: embed.Build(), components: components.Build(), ephemeral: true);
return;
@@ -57,7 +57,7 @@ public class ServerListCommand : BaseModule
components.WithButton("Panel",
emote: Emote.Parse("<a:Earth:1092814004113657927>"),
style: ButtonStyle.Link,
url: $"{ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")}");
url: $"{ConfigService.Get().Moonlight.AppUrl}");
if (servers.Count > 25)
{

View File

@@ -44,10 +44,10 @@ public DiscordBotService(
ServiceScope = ServiceScopeFactory.CreateScope();
var discordConfig = ConfigService
.GetSection("Moonlight")
.GetSection("DiscordBot");
.Get()
.Moonlight.DiscordBot;
if (!discordConfig.GetValue<bool>("Enable"))
if (!discordConfig.Enable)
return;
Client.Log += Log;
@@ -67,7 +67,7 @@ public DiscordBotService(
await ActivityStatusModule.UpdateActivityStatusList();
await Client.LoginAsync(TokenType.Bot, discordConfig.GetValue<string>("Token"));
await Client.LoginAsync(TokenType.Bot, discordConfig.Token);
await Client.StartAsync();
await Task.Delay(-1);

View File

@@ -87,8 +87,8 @@ public class EmbedBuilderModule : BaseModule
int[] randomNumbers = new int[] { 1, 3, 8, 11, 20 };
if (randomNumbers.Contains(random.Next(1, 24)))
return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username + " - The Rick version").WithUrl(ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")).WithIconUrl("https://cdn.discordapp.com/attachments/750696464014901268/1092783310129860618/rick.gif");
return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username + " - The Rick version").WithUrl(ConfigService.Get().Moonlight.AppUrl).WithIconUrl("https://cdn.discordapp.com/attachments/750696464014901268/1092783310129860618/rick.gif");
return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username).WithUrl(ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")).WithIconUrl(Client.CurrentUser.GetAvatarUrl());
return new EmbedAuthorBuilder().WithName(Client.CurrentUser.Username).WithUrl(ConfigService.Get().Moonlight.AppUrl).WithIconUrl(Client.CurrentUser.GetAvatarUrl());
}
}

View File

@@ -72,7 +72,7 @@ public class ServerListComponentHandlerModule : BaseModule
// stopping
// offline
// installing
if (!ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("PowerActions") && costomId[1] is "Start" or "Restart" or "Stop" or "Kill" or "Update")
if (!ConfigService.Get().Moonlight.DiscordBot.PowerActions && costomId[1] is "Start" or "Restart" or "Stop" or "Kill" or "Update")
{
embed = dcs.EmbedBuilderModule.StandardEmbed($"This feature is disabled for Security reasons! \n If you believe this is a error please contact the Administrators from this panel.", Color.Red, component.User);
await component.RespondAsync(embed: embed.Build(), ephemeral: true);
@@ -80,7 +80,7 @@ public class ServerListComponentHandlerModule : BaseModule
return;
}
if (!ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("SendCommands") && costomId[1] is "SendCommand")
if (!ConfigService.Get().Moonlight.DiscordBot.SendCommands && costomId[1] is "SendCommand")
{
embed = dcs.EmbedBuilderModule.StandardEmbed($"This feature is disabled for Security reasons! \n If you believe this is a error please contact the Administrators from this panel.", Color.Red, component.User);
await component.RespondAsync(embed: embed.Build(), ephemeral: true);
@@ -302,7 +302,7 @@ public class ServerListComponentHandlerModule : BaseModule
{
embed = dcs.EmbedBuilderModule.StandardEmbed("Sorry ;( \n Please first create and/or link a Account to Discord! \n Press the Button to register/log in.", Color.Red, component.User);
components = new ComponentBuilder();
components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.GetSection("Moonlight").GetValue<String>("AppUrl"));
components.WithButton("Click Here", style: ButtonStyle.Link, url: ConfigService.Get().Moonlight.AppUrl);
await component.RespondAsync(embed: embed.Build(), components: components.Build(), ephemeral: true);
return;
@@ -332,7 +332,7 @@ public class ServerListComponentHandlerModule : BaseModule
components.WithButton("Panel",
emote: Emote.Parse("<a:Earth:1092814004113657927>"),
style: ButtonStyle.Link,
url: $"{ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")}");
url: $"{ConfigService.Get().Moonlight.AppUrl}");
components.WithButton("Previous-page",
emote: Emote.Parse("<:ArrowLeft:1101547474180649030>"),
@@ -378,7 +378,7 @@ public class ServerListComponentHandlerModule : BaseModule
var components = new ComponentBuilder();
if (ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("PowerActions"))
if (ConfigService.Get().Moonlight.DiscordBot.PowerActions)
{
components.WithButton("Start", style: ButtonStyle.Success, customId: $"Sm.Start.{server.Id}", disabled: false);
components.WithButton("Restart", style: ButtonStyle.Primary, customId: $"Sm.Restart.{server.Id}", disabled: false);
@@ -389,14 +389,14 @@ public class ServerListComponentHandlerModule : BaseModule
components.WithButton("Way2Server",
emote: Emote.Parse("<a:Earth:1092814004113657927>"),
style: ButtonStyle.Link,
url: $"{ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl")}/server/{server.Uuid}");
url: $"{ConfigService.Get().Moonlight.AppUrl}/server/{server.Uuid}");
components.WithButton("Update",
emote: Emote.Parse("<:refresh:1101547898803605605>"),
style: ButtonStyle.Secondary,
customId: $"Sm.Update.{server.Id}");
if (ConfigService.GetSection("Moonlight").GetSection("DiscordBot").GetValue<bool>("SendCommands"))
if (ConfigService.Get().Moonlight.DiscordBot.SendCommands)
{
components.WithButton("SendCommand",
emote: Emote.Parse("<:Console:1101547358157819944>"),

View File

@@ -1,5 +1,6 @@
using CloudFlare.Client;
using CloudFlare.Client.Api.Authentication;
using CloudFlare.Client.Api.Display;
using CloudFlare.Client.Api.Parameters.Data;
using CloudFlare.Client.Api.Result;
using CloudFlare.Client.Api.Zones;
@@ -12,6 +13,7 @@ using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories.Domains;
using DnsRecord = Moonlight.App.Models.Misc.DnsRecord;
using MatchType = CloudFlare.Client.Enumerators.MatchType;
namespace Moonlight.App.Services;
@@ -31,15 +33,15 @@ public class DomainService
SharedDomainRepository = sharedDomainRepository;
var config = configService
.GetSection("Moonlight")
.GetSection("Domains");
.Get()
.Moonlight.Domains;
AccountId = config.GetValue<string>("AccountId");
AccountId = config.AccountId;
Client = new(
new ApiKeyAuthentication(
config.GetValue<string>("Email"),
config.GetValue<string>("Key")
config.Email,
config.Key
)
);
}
@@ -93,9 +95,36 @@ public class DomainService
{
var domain = EnsureData(d);
var records = GetData(
await Client.Zones.DnsRecords.GetAsync(domain.SharedDomain.CloudflareId)
var records = new List<CloudFlare.Client.Api.Zones.DnsRecord.DnsRecord>();
// Load paginated
// TODO: Find an alternative option. This way to load the records is NOT optimal
// and can result in long loading time when there are many dns records.
// As cloudflare does not offer a way to search dns records which starts
// with a specific string we are not able to filter it using the api (client)
var initialResponse = await Client.Zones.DnsRecords.GetAsync(domain.SharedDomain.CloudflareId);
records.AddRange(GetData(initialResponse));
// Check if there are more pages
while (initialResponse.ResultInfo.Page < initialResponse.ResultInfo.TotalPage)
{
// Get the next page of data
var nextPageResponse = await Client.Zones.DnsRecords.GetAsync(
domain.SharedDomain.CloudflareId,
displayOptions: new()
{
Page = initialResponse.ResultInfo.Page + 1
}
);
var nextPageRecords = GetData(nextPageResponse);
// Append the records from the next page to the existing records
records.AddRange(nextPageRecords);
// Update the initial response to the next page response
initialResponse = nextPageResponse;
}
var rname = $"{domain.Name}.{domain.SharedDomain.Name}";
var dname = $".{rname}";
@@ -145,7 +174,11 @@ public class DomainService
if (dnsRecord.Type == DnsRecordType.Srv)
{
var parts = dnsRecord.Name.Split(".");
Enum.TryParse(parts[1], out Protocol protocol);
Protocol protocol = Protocol.Tcp;
if (parts[1].Contains("udp"))
protocol = Protocol.Udp;
var valueParts = dnsRecord.Content.Split(" ");

View File

@@ -8,7 +8,7 @@ public class ResourceService
public ResourceService(ConfigService configService)
{
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
AppUrl = configService.Get().Moonlight.AppUrl;
}
public string Image(string name)

View File

@@ -15,6 +15,7 @@ public class StorageService
Directory.CreateDirectory(PathBuilder.Dir("storage", "configs"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "resources"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "backups"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
if(IsEmpty(PathBuilder.Dir("storage", "resources")))
{

View File

@@ -1,21 +1,40 @@
using CurrieTechnologies.Razor.SweetAlert2;
using Microsoft.JSInterop;
namespace Moonlight.App.Services.Interop;
public class AlertService
{
private readonly SweetAlertService SweetAlertService;
private readonly SmartTranslateService SmartTranslateService;
private readonly IJSRuntime JsRuntime;
private SweetAlertService? SweetAlertService;
public AlertService(SweetAlertService service, SmartTranslateService smartTranslateService)
public AlertService(SmartTranslateService smartTranslateService, IJSRuntime jsRuntime)
{
SweetAlertService = service;
SmartTranslateService = smartTranslateService;
JsRuntime = jsRuntime;
}
// We create the swal service here and not using the dependency injection
// because it initializes when instantiated which leads to js invoke errors
private Task EnsureService()
{
if (SweetAlertService == null)
{
SweetAlertService = new(JsRuntime, new()
{
Theme = SweetAlertTheme.Dark
});
}
return Task.CompletedTask;
}
public async Task Info(string title, string desciption)
{
await SweetAlertService.FireAsync(new SweetAlertOptions()
await EnsureService();
await SweetAlertService!.FireAsync(new SweetAlertOptions()
{
Title = title,
Text = desciption,
@@ -30,7 +49,9 @@ public class AlertService
public async Task Success(string title, string desciption)
{
await SweetAlertService.FireAsync(new SweetAlertOptions()
await EnsureService();
await SweetAlertService!.FireAsync(new SweetAlertOptions()
{
Title = title,
Text = desciption,
@@ -45,7 +66,9 @@ public class AlertService
public async Task Warning(string title, string desciption)
{
await SweetAlertService.FireAsync(new SweetAlertOptions()
await EnsureService();
await SweetAlertService!.FireAsync(new SweetAlertOptions()
{
Title = title,
Text = desciption,
@@ -60,7 +83,9 @@ public class AlertService
public async Task Error(string title, string desciption)
{
await SweetAlertService.FireAsync(new SweetAlertOptions()
await EnsureService();
await SweetAlertService!.FireAsync(new SweetAlertOptions()
{
Title = title,
Text = desciption,
@@ -75,7 +100,9 @@ public class AlertService
public async Task<bool> YesNo(string title, string desciption, string yesText, string noText)
{
var result = await SweetAlertService.FireAsync(new SweetAlertOptions()
await EnsureService();
var result = await SweetAlertService!.FireAsync(new SweetAlertOptions()
{
Title = title,
Text = desciption,
@@ -91,7 +118,9 @@ public class AlertService
public async Task<string?> Text(string title, string desciption, string setValue)
{
var result = await SweetAlertService.FireAsync(new SweetAlertOptions()
await EnsureService();
var result = await SweetAlertService!.FireAsync(new SweetAlertOptions()
{
Title = title,
Text = desciption,

View File

@@ -25,16 +25,15 @@ public class ReCaptchaService
ConfigService = configService;
var recaptchaConfig = ConfigService
.GetSection("Moonlight")
.GetSection("Security")
.GetSection("ReCaptcha");
.Get()
.Moonlight.Security.ReCaptcha;
Enable = recaptchaConfig.GetValue<bool>("Enable");
Enable = recaptchaConfig.Enable;
if (Enable)
{
SiteKey = recaptchaConfig.GetValue<string>("SiteKey");
SecretKey = recaptchaConfig.GetValue<string>("SecretKey");
SiteKey = recaptchaConfig.SiteKey;
SecretKey = recaptchaConfig.SecretKey;
}
}

View File

@@ -17,14 +17,14 @@ public class MailService
public MailService(ConfigService configService)
{
var mailConfig = configService
.GetSection("Moonlight")
.GetSection("Mail");
.Get()
.Moonlight.Mail;
Server = mailConfig.GetValue<string>("Server");
Password = mailConfig.GetValue<string>("Password");
Email = mailConfig.GetValue<string>("Email");
Port = mailConfig.GetValue<int>("Port");
Ssl = mailConfig.GetValue<bool>("Ssl");
Server = mailConfig.Server;
Password = mailConfig.Password;
Email = mailConfig.Email;
Port = mailConfig.Port;
Ssl = mailConfig.Ssl;
}
public async Task SendMail(

View File

@@ -1,4 +1,5 @@
using Moonlight.App.Database.Entities;
using Mappy.Net;
using Moonlight.App.Database.Entities;
using Moonlight.App.Exceptions;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
@@ -24,14 +25,17 @@ public class OAuth2Service
ConfigService = configService;
ServiceScopeFactory = serviceScopeFactory;
var config = ConfigService.GetSection("Moonlight").GetSection("OAuth2");
var config = ConfigService
.Get()
.Moonlight.OAuth2;
Configs = config.GetSection("Providers").Get<OAuth2ProviderConfig[]>()
?? Array.Empty<OAuth2ProviderConfig>();
Configs = config.Providers
.Select(Mapper.Map<OAuth2ProviderConfig>)
.ToArray();
OverrideUrl = config.GetValue<string>("OverrideUrl");
EnableOverrideUrl = config.GetValue<bool>("EnableOverrideUrl");
AppUrl = configService.GetSection("Moonlight").GetValue<string>("AppUrl");
OverrideUrl = config.OverrideUrl;
EnableOverrideUrl = config.EnableOverrideUrl;
AppUrl = configService.Get().Moonlight.AppUrl;
// Register additional providers here
RegisterOAuth2<DiscordOAuth2Provider>("discord");

View File

@@ -25,10 +25,9 @@ public class OneTimeJwtService
var opt = new Dictionary<string, string>();
options.Invoke(opt);
string secret = ConfigService
.GetSection("Moonlight")
.GetSection("Security")
.GetValue<string>("Token");
var secret = ConfigService
.Get()
.Moonlight.Security.Token;
var id = StringHelper.GenerateString(16);
@@ -55,10 +54,9 @@ public class OneTimeJwtService
public async Task<Dictionary<string, string>?> Validate(string token)
{
string secret = ConfigService
.GetSection("Moonlight")
.GetSection("Security")
.GetValue<string>("Token");
var secret = ConfigService
.Get()
.Moonlight.Security.Token;
string json;

View File

@@ -26,12 +26,12 @@ public class RatingService
Event = eventSystem;
UserRepository = userRepository;
var config = configService.GetSection("Moonlight").GetSection("Rating");
var config = configService.Get().Moonlight.Rating;
Enabled = config.GetValue<bool>("Enabled");
Url = config.GetValue<string>("Url");
MinRating = config.GetValue<int>("MinRating");
DaysSince = config.GetValue<int>("DaysSince");
Enabled = config.Enabled;
Url = config.Url;
MinRating = config.MinRating;
DaysSince = config.DaysSince;
}
public async Task<bool> ShouldRate()

View File

@@ -79,10 +79,20 @@ public class ServerService
{
Server server = EnsureNodeData(s);
return await WingsApiHelper.Get<ServerDetails>(
ServerDetails result = null!;
await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{
result = await WingsApiHelper.Get<ServerDetails>(
server.Node,
$"api/servers/{server.Uuid}"
);
});
return result;
}
public async Task SetPowerState(Server s, PowerSignal signal)
@@ -109,7 +119,8 @@ public class ServerService
var backup = new ServerBackup()
{
Name = $"Created at {DateTimeService.GetCurrent().ToShortDateString()} {DateTimeService.GetCurrent().ToShortTimeString()}",
Name =
$"Created at {DateTimeService.GetCurrent().ToShortDateString()} {DateTimeService.GetCurrent().ToShortTimeString()}",
Uuid = Guid.NewGuid(),
CreatedAt = DateTimeService.GetCurrent(),
Created = false
@@ -175,8 +186,15 @@ public class ServerService
try
{
await WingsApiHelper.Delete(serverData.Node, $"api/servers/{serverData.Uuid}/backup/{serverBackup.Uuid}",
await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{
await WingsApiHelper.Delete(serverData.Node,
$"api/servers/{serverData.Uuid}/backup/{serverBackup.Uuid}",
null);
});
}
catch (WingsException e)
{
@@ -257,7 +275,8 @@ public class ServerService
freeAllocations = NodeAllocationRepository
.Get()
.FromSqlRaw($"SELECT * FROM `NodeAllocations` WHERE ServerId IS NULL AND NodeId={node.Id} LIMIT {allocations}")
.FromSqlRaw(
$"SELECT * FROM `NodeAllocations` WHERE ServerId IS NULL AND NodeId={node.Id} LIMIT {allocations}")
.ToArray();
}
catch (Exception)
@@ -285,7 +304,8 @@ public class ServerService
Allocations = freeAllocations.ToList(),
Backups = new(),
OverrideStartup = "",
DockerImageIndex = image.DockerImages.FindIndex(x => x.Default)
DockerImageIndex = image.DockerImages.FindIndex(x => x.Default),
Installing = true
};
foreach (var imageVariable in image.Variables)
@@ -303,12 +323,18 @@ public class ServerService
var newServerData = ServerRepository.Add(server);
try
{
await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{
await WingsApiHelper.Post(node, $"api/servers", new CreateServer()
{
Uuid = newServerData.Uuid,
StartOnCompletion = false
});
});
//TODO: AuditLog
@@ -369,8 +395,6 @@ public class ServerService
public async Task Delete(Server s)
{
throw new DisplayException("Deleting servers is currently disabled");
var backups = await GetBackups(s);
foreach (var backup in backups)
@@ -391,7 +415,21 @@ public class ServerService
.Include(x => x.Node)
.First(x => x.Id == s.Id);
try
{
await new Retry()
.Times(3)
.At(x => x.Message.Contains("A task was canceled"))
.Call(async () =>
{
await WingsApiHelper.Delete(server.Node, $"api/servers/{server.Uuid}", null);
});
}
catch (WingsException e)
{
if (e.StatusCode != 404)
throw;
}
foreach (var variable in server.Variables.ToArray())
{

View File

@@ -30,9 +30,8 @@ public class IdentityService
HttpContextAccessor = httpContextAccessor;
Secret = configService
.GetSection("Moonlight")
.GetSection("Security")
.GetValue<string>("Token");
.Get()
.Moonlight.Security.Token;
}
public async Task<User?> Get()
@@ -120,6 +119,10 @@ public class IdentityService
return null;
UserCache = user;
user.LastIp = GetIp();
UserRepository.Update(user);
return UserCache;
}
catch (Exception e)

View File

@@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Moonlight.App.Database.Entities;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Interop;
namespace Moonlight.App.Services.Sessions;
public class SessionClientService
{
public readonly Guid Uuid = Guid.NewGuid();
public readonly DateTime CreateTimestamp = DateTime.UtcNow;
public User? User { get; private set; }
public readonly IdentityService IdentityService;
public readonly AlertService AlertService;
public readonly NavigationManager NavigationManager;
public readonly IJSRuntime JsRuntime;
private readonly SessionServerService SessionServerService;
private readonly Repository<User> UserRepository;
public SessionClientService(
IdentityService identityService,
AlertService alertService,
NavigationManager navigationManager,
IJSRuntime jsRuntime,
SessionServerService sessionServerService,
Repository<User> userRepository)
{
IdentityService = identityService;
AlertService = alertService;
NavigationManager = navigationManager;
JsRuntime = jsRuntime;
SessionServerService = sessionServerService;
UserRepository = userRepository;
}
public async Task Start()
{
User = await IdentityService.Get();
if (User != null) // Track users last visit
{
User.LastVisitedAt = DateTime.UtcNow;
UserRepository.Update(User);
}
await SessionServerService.Register(this);
}
public async Task Stop()
{
await SessionServerService.UnRegister(this);
}
}

View File

@@ -0,0 +1,64 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Events;
namespace Moonlight.App.Services.Sessions;
public class SessionServerService
{
private readonly List<SessionClientService> Sessions = new();
private readonly EventSystem Event;
public SessionServerService(EventSystem eventSystem)
{
Event = eventSystem;
}
public async Task Register(SessionClientService sessionClientService)
{
lock (Sessions)
{
if(!Sessions.Contains(sessionClientService))
Sessions.Add(sessionClientService);
}
await Event.Emit("sessions.add", sessionClientService);
}
public async Task UnRegister(SessionClientService sessionClientService)
{
lock (Sessions)
{
if(Sessions.Contains(sessionClientService))
Sessions.Remove(sessionClientService);
}
await Event.Emit("sessions.remove", sessionClientService);
}
public Task<SessionClientService[]> GetSessions()
{
lock (Sessions)
{
return Task.FromResult(Sessions.ToArray());
}
}
public async Task ReloadUserSessions(User user)
{
var sessions = await GetSessions();
foreach (var session in sessions)
{
if (session.User != null && session.User.Id == user.Id)
{
try
{
session.NavigationManager.NavigateTo(session.NavigationManager.Uri, true);
}
catch (Exception)
{
// ignore
}
}
}
}
}

View File

@@ -1,83 +0,0 @@
using Microsoft.AspNetCore.Components;
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Misc;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Interop;
namespace Moonlight.App.Services.Sessions;
public class SessionService
{
private readonly SessionRepository SessionRepository;
private Repository<User> UserRepository;
private readonly IdentityService IdentityService;
private readonly NavigationManager NavigationManager;
private readonly AlertService AlertService;
private readonly DateTimeService DateTimeService;
private Session? OwnSession;
public SessionService(
SessionRepository sessionRepository,
IdentityService identityService,
NavigationManager navigationManager,
AlertService alertService,
DateTimeService dateTimeService,
Repository<User> userRepository)
{
SessionRepository = sessionRepository;
IdentityService = identityService;
NavigationManager = navigationManager;
AlertService = alertService;
DateTimeService = dateTimeService;
UserRepository = userRepository;
}
public async Task Register()
{
var user = await IdentityService.Get();
OwnSession = new Session()
{
Ip = IdentityService.GetIp(),
Url = NavigationManager.Uri,
Device = IdentityService.GetDevice(),
CreatedAt = DateTimeService.GetCurrent(),
User = user,
Navigation = NavigationManager,
AlertService = AlertService
};
SessionRepository.Add(OwnSession);
if (user != null) // Track last session init of user as last visited timestamp
{
user.LastVisitedAt = DateTimeService.GetCurrent();
UserRepository.Update(user);
}
}
public void Refresh()
{
OwnSession.Url = NavigationManager.Uri;
}
public void Close()
{
SessionRepository.Delete(OwnSession);
}
public Session[] GetAll()
{
return SessionRepository.Get();
}
public void ReloadUserSessions(User user)
{
foreach (var session in SessionRepository.Get())
{
if(session.User != null && session.User.Id == user.Id)
session.Navigation.NavigateTo(session.Navigation.Uri, true);
}
}
}

View File

@@ -28,13 +28,12 @@ public class SmartDeployService
public async Task<Node?> GetNode()
{
var config = ConfigService
.GetSection("Moonlight")
.GetSection("SmartDeploy")
.GetSection("Server");
.Get()
.Moonlight.SmartDeploy.Server;
if (config.GetValue<bool>("EnableOverride"))
if (config.EnableOverride)
{
var nodeId = config.GetValue<int>("OverrideNode");
var nodeId = config.OverrideNode;
return NodeRepository.Get().FirstOrDefault(x => x.Id == nodeId);
}

View File

@@ -17,13 +17,13 @@ public class StatisticsCaptureService
DateTimeService = dateTimeService;
var config = configService
.GetSection("Moonlight")
.GetSection("Statistics");
.Get()
.Moonlight.Statistics;
if(!config.GetValue<bool>("Enabled"))
if(!config.Enabled)
return;
var period = TimeSpan.FromMinutes(config.GetValue<int>("Wait"));
var period = TimeSpan.FromMinutes(config.Wait);
Timer = new(period);
Logger.Info("Starting statistics system");
@@ -36,8 +36,6 @@ public class StatisticsCaptureService
{
while (await Timer.WaitForNextTickAsync())
{
Logger.Warn("Creating statistics");
using var scope = ServiceScopeFactory.CreateScope();
var statisticsRepo = scope.ServiceProvider.GetRequiredService<Repository<StatisticsData>>();
@@ -46,7 +44,7 @@ public class StatisticsCaptureService
var domainsRepo = scope.ServiceProvider.GetRequiredService<Repository<Domain>>();
var webspacesRepo = scope.ServiceProvider.GetRequiredService<Repository<WebSpace>>();
var databasesRepo = scope.ServiceProvider.GetRequiredService<Repository<MySqlDatabase>>();
var sessionService = scope.ServiceProvider.GetRequiredService<SessionService>();
var sessionService = scope.ServiceProvider.GetRequiredService<SessionServerService>();
void AddEntry(string chart, int value)
{
@@ -63,7 +61,7 @@ public class StatisticsCaptureService
AddEntry("domainsCount", domainsRepo.Get().Count());
AddEntry("webspacesCount", webspacesRepo.Get().Count());
AddEntry("databasesCount", databasesRepo.Get().Count());
AddEntry("sessionsCount", sessionService.GetAll().Length);
AddEntry("sessionsCount", (await sessionService.GetSessions()).Length);
}
}
catch (Exception e)

View File

@@ -1,4 +1,5 @@
using Moonlight.App.Repositories;
using Moonlight.App.Exceptions;
using Moonlight.App.Repositories;
using Moonlight.App.Services.Sessions;
using OtpNet;
@@ -38,21 +39,24 @@ public class TotpService
return user!.TotpSecret;
}
public async Task Enable()
public async Task GenerateSecret()
{
var user = (await IdentityService.Get())!;
user.TotpSecret = GenerateSecret();
user.TotpSecret = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));;
UserRepository.Update(user);
//TODO: AuditLog
}
public async Task EnforceTotpLogin()
public async Task Enable(string code)
{
var user = (await IdentityService.Get())!;
if (!await Verify(user.TotpSecret, code))
{
throw new DisplayException("The 2fa code you entered is invalid");
}
user.TotpEnabled = true;
UserRepository.Update(user);
}
@@ -62,14 +66,10 @@ public class TotpService
var user = (await IdentityService.Get())!;
user.TotpEnabled = false;
user.TotpSecret = "";
UserRepository.Update(user);
//TODO: AuditLog
}
private string GenerateSecret()
{
return Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));
}
}

View File

@@ -38,9 +38,8 @@ public class UserService
DateTimeService = dateTimeService;
JwtSecret = configService
.GetSection("Moonlight")
.GetSection("Security")
.GetValue<string>("Token");
.Get()
.Moonlight.Security.Token;
}
public async Task<string> Register(string email, string password, string firstname, string lastname)
@@ -73,7 +72,9 @@ public class UserService
TotpEnabled = false,
TotpSecret = "",
UpdatedAt = DateTimeService.GetCurrent(),
TokenValidTime = DateTimeService.GetCurrent().AddDays(-5)
TokenValidTime = DateTimeService.GetCurrent().AddDays(-5),
LastIp = IdentityService.GetIp(),
RegisterIp = IdentityService.GetIp()
});
await MailService.SendMail(user!, "register", values => {});

View File

@@ -18,7 +18,7 @@
<PackageReference Include="BlazorMonaco" Version="2.1.0" />
<PackageReference Include="BlazorTable" Version="1.17.0" />
<PackageReference Include="CloudFlare.Client" Version="6.1.4" />
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.4.0" />
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.5.0" />
<PackageReference Include="Discord.Net" Version="3.10.0" />
<PackageReference Include="Discord.Net.Webhook" Version="3.10.0" />
<PackageReference Include="DnsClient" Version="1.7.0" />
@@ -46,7 +46,10 @@
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="RestSharp" Version="109.0.0-preview.1" />
<PackageReference Include="Sentry.AspNetCore" Version="3.33.1" />
<PackageReference Include="Sentry.Serilog" Version="3.33.1" />
<PackageReference Include="Serilog" Version="3.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.1-dev-00910" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.1-dev-00947" />
<PackageReference Include="SSH.NET" Version="2020.0.2" />
@@ -100,4 +103,12 @@
<AdditionalFiles Include="Shared\Views\Server\Settings\ServerResetSetting.razor" />
</ItemGroup>
<ItemGroup>
<Content Update="storage\configs\config.json.bak">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -11,12 +11,12 @@
@{
var headerConfig = ConfigService
.GetSection("Moonlight")
.GetSection("Html")
.GetSection("Headers");
.Get()
.Moonlight.Html.Headers;
var moonlightConfig = ConfigService
.GetSection("Moonlight");
.Get()
.Moonlight;
}
<!DOCTYPE html>
@@ -26,16 +26,16 @@
<meta property="og:locale" content="de_DE"/>
<meta property="og:type" content="article"/>
<meta content="@(headerConfig.GetValue<string>("Title"))" property="og:title"/>
<meta content="@(headerConfig.GetValue<string>("Description"))" property="og:description"/>
<meta content="@(moonlightConfig.GetValue<string>("AppUrl"))" property="og:url"/>
<meta content="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logolong.png" property="og:image"/>
<meta content="@(headerConfig.GetValue<string>("Color"))" data-react-helmet="true" name="theme-color"/>
<meta content="@(headerConfig.Title)" property="og:title"/>
<meta content="@(headerConfig.Description)" property="og:description"/>
<meta content="@(moonlightConfig.AppUrl)" property="og:url"/>
<meta content="@(moonlightConfig.AppUrl)/api/moonlight/resources/images/logolong.png" property="og:image"/>
<meta content="@(headerConfig.Color)" data-react-helmet="true" name="theme-color"/>
<meta content="@(headerConfig.GetValue<string>("Description"))" name="description"/>
<meta content="@(headerConfig.GetValue<string>("Keywords"))" name="keywords"/>
<meta content="@(headerConfig.Description)" name="description"/>
<meta content="@(headerConfig.Keywords)" name="keywords"/>
<link rel="shortcut icon" href="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg"/>
<link rel="shortcut icon" href="@(moonlightConfig.AppUrl)/api/moonlight/resources/images/logo.svg"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"/>
@@ -75,7 +75,7 @@
<div id="flashbang" class="flashbanglight"></div>
<div class="app-page-loader flex-column">
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg" class="h-25px"/>
<img alt="Logo" src="@(moonlightConfig.AppUrl)/api/moonlight/resources/images/logo.svg" class="h-25px"/>
@{
string loadingMessage;
@@ -96,13 +96,14 @@
</div>
</div>
<script src="/_framework/blazor.server.js"></script>
<script src="/assets/plugins/global/plugins.bundle.js"></script>
<script src="/_content/XtermBlazor/XtermBlazor.min.js"></script>
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
<script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
<script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@@shopify/draggable@1.0.0-beta.11/lib/draggable.bundle.js"></script>
<script src="https://www.google.com/recaptcha/api.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.min.js"></script>
@@ -122,5 +123,7 @@ moonlight.loading.registerXterm();
<script src="_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
<script src="/_framework/blazor.server.js"></script>
</body>
</html>

View File

@@ -6,6 +6,7 @@ using Moonlight.App.ApiClients.CloudPanel;
using Moonlight.App.ApiClients.Daemon;
using Moonlight.App.ApiClients.Modrinth;
using Moonlight.App.ApiClients.Paper;
using Moonlight.App.ApiClients.Telemetry;
using Moonlight.App.ApiClients.Wings;
using Moonlight.App.Database;
using Moonlight.App.Diagnostics.HealthChecks;
@@ -29,8 +30,9 @@ using Moonlight.App.Services.Notifications;
using Moonlight.App.Services.Sessions;
using Moonlight.App.Services.Statistics;
using Moonlight.App.Services.SupportChat;
using Sentry;
using Serilog;
using Serilog.Sinks.SystemConsole.Themes;
using Serilog.Events;
namespace Moonlight
{
@@ -40,14 +42,53 @@ namespace Moonlight
{
// This will also copy all default config files
var configService = new ConfigService(new StorageService());
var shouldUseSentry = configService
.Get()
.Moonlight.Sentry.Enable;
if (configService.DebugMode)
{
if (shouldUseSentry)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
.WriteTo.File(PathBuilder.File("storage", "logs", $"{DateTime.UtcNow:yyyy-MM-dd}.log"))
.WriteTo.Sentry(options =>
{
options.MinimumBreadcrumbLevel = LogEventLevel.Debug;
options.MinimumEventLevel = LogEventLevel.Warning;
})
.CreateLogger();
}
else
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
.WriteTo.File(PathBuilder.File("storage", "logs", $"{DateTime.UtcNow:yyyy-MM-dd}.log"))
.CreateLogger();
}
}
else
{
if (shouldUseSentry)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
.WriteTo.Sentry(options =>
{
options.MinimumBreadcrumbLevel = LogEventLevel.Information;
options.MinimumEventLevel = LogEventLevel.Warning;
})
.WriteTo.File(PathBuilder.File("storage", "logs", $"{DateTime.UtcNow:yyyy-MM-dd}.log"))
.CreateLogger();
}
else
@@ -57,8 +98,10 @@ namespace Moonlight
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}")
.WriteTo.File(PathBuilder.File("storage", "logs", $"{DateTime.UtcNow:yyyy-MM-dd}.log"))
.CreateLogger();
}
}
Logger.Info($"Working dir: {Directory.GetCurrentDirectory()}");
@@ -71,8 +114,8 @@ namespace Moonlight
// Switch to logging.net injection
// TODO: Enable in production
//builder.Logging.ClearProviders();
//builder.Logging.AddProvider(new LogMigratorProvider());
builder.Logging.ClearProviders();
builder.Logging.AddProvider(new LogMigratorProvider());
// Add services to the container.
builder.Services.AddRazorPages();
@@ -89,11 +132,27 @@ namespace Moonlight
.AddCheck<NodeHealthCheck>("Nodes")
.AddCheck<DaemonHealthCheck>("Daemons");
// Sentry
if (shouldUseSentry)
{
builder.WebHost.UseSentry(options =>
{
options.Dsn = configService
.Get()
.Moonlight.Sentry.Dsn;
options.Debug = configService.DebugMode;
options.DiagnosticLevel = SentryLevel.Warning;
options.TracesSampleRate = 1.0;
options.DiagnosticLogger = new SentryDiagnosticsLogger(SentryLevel.Warning);
});
}
// Databases
builder.Services.AddDbContext<DataContext>();
// Repositories
builder.Services.AddSingleton<SessionRepository>();
builder.Services.AddScoped<UserRepository>();
builder.Services.AddScoped<NodeRepository>();
builder.Services.AddScoped<ServerRepository>();
@@ -120,7 +179,6 @@ namespace Moonlight
builder.Services.AddScoped<CookieService>();
builder.Services.AddScoped<IdentityService>();
builder.Services.AddScoped<IpLocateService>();
builder.Services.AddScoped<SessionService>();
builder.Services.AddScoped<AlertService>();
builder.Services.AddScoped<SmartTranslateService>();
builder.Services.AddScoped<UserService>();
@@ -156,6 +214,9 @@ namespace Moonlight
builder.Services.AddScoped<SubscriptionService>();
builder.Services.AddScoped<SubscriptionAdminService>();
builder.Services.AddScoped<SessionClientService>();
builder.Services.AddSingleton<SessionServerService>();
// Loggers
builder.Services.AddScoped<MailService>();
builder.Services.AddSingleton<TrashMailDetectorService>();
@@ -176,6 +237,7 @@ namespace Moonlight
builder.Services.AddScoped<DaemonApiHelper>();
builder.Services.AddScoped<CloudPanelApiHelper>();
builder.Services.AddScoped<ModrinthApiHelper>();
builder.Services.AddScoped<TelemetryApiHelper>();
// Background services
builder.Services.AddSingleton<DiscordBotService>();
@@ -183,13 +245,13 @@ namespace Moonlight
builder.Services.AddSingleton<DiscordNotificationService>();
builder.Services.AddSingleton<CleanupService>();
builder.Services.AddSingleton<MalwareScanService>();
builder.Services.AddSingleton<TelemetryService>();
// Other
builder.Services.AddSingleton<MoonlightService>();
// Third party services
builder.Services.AddBlazorTable();
builder.Services.AddSweetAlert2(options => { options.Theme = SweetAlertTheme.Dark; });
builder.Services.AddBlazorContextMenu();
builder.Services.AddBlazorDownloadFile();
@@ -203,6 +265,12 @@ namespace Moonlight
app.UseHsts();
}
// Sentry
if (shouldUseSentry)
{
app.UseSentryTracing();
}
app.UseStaticFiles();
app.UseRouting();
app.UseWebSockets();
@@ -222,6 +290,7 @@ namespace Moonlight
_ = app.Services.GetRequiredService<StatisticsCaptureService>();
_ = app.Services.GetRequiredService<DiscordNotificationService>();
_ = app.Services.GetRequiredService<MalwareScanService>();
_ = app.Services.GetRequiredService<TelemetryService>();
_ = app.Services.GetRequiredService<MoonlightService>();

View File

@@ -1,16 +1,12 @@
@using Moonlight.App.Services
@using Moonlight.App.Services.Files
@inject ConfigService ConfigService
@{
var moonlightConfig = ConfigService
.GetSection("Moonlight");
}
@inject ResourceService ResourceService
<div class="card card-flush w-lg-650px py-5">
<div class="card-body py-15 py-lg-20">
<div class="mb-14">
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logolong.png" class="h-40px">
<img alt="Logo" src="@(ResourceService.Image("logolong.png"))" class="h-40px">
</div>
<h1 class="fw-bolder text-gray-900 mb-5"><TL>Your account is banned from moonlight</TL></h1>
<div class="fw-semibold fs-6 text-gray-500 mb-8">

View File

@@ -1,16 +1,12 @@
@using Moonlight.App.Services
@using Moonlight.App.Services.Files
@inject ConfigService ConfigService
@{
var moonlightConfig = ConfigService
.GetSection("Moonlight");
}
@inject ResourceService ResourceService
<div class="card card-flush w-lg-650px py-5">
<div class="card-body py-15 py-lg-20">
<div class="mb-14">
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logolong.png" class="h-40px">
<img alt="Logo" src="@(ResourceService.Image("logolong.png"))" class="h-40px">
</div>
<h1 class="fw-bolder text-gray-900 mb-5"><TL>Your moonlight account is disabled</TL></h1>
<div class="fw-semibold fs-6 text-gray-500 mb-8">

View File

@@ -96,7 +96,7 @@
{
<SmartForm Model="TotpData" OnValidSubmit="DoLogin">
<div class="fv-row mb-8 fv-plugins-icon-container">
<InputText @bind-Value="TotpData.Code" type="number" class="form-control bg-transparent"></InputText>
<InputText @bind-Value="TotpData.Code" type="number" class="form-control bg-transparent" placeholder="@(SmartTranslateService.Translate("2fa code"))"></InputText>
</div>
<div class="d-grid mb-10">
<button type="submit" class="btn btn-primary">

View File

@@ -0,0 +1,87 @@
@using Moonlight.App.Helpers.Files
@using Moonlight.App.Services.Interop
@inject ModalService ModalService
<div class="modal fade" id="connectionDetails" tabindex="-1" aria-labelledby="connectionDetails" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<TL>Connection details</TL>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Host</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Host)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Port</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Port)">
</div>
</div>
<div class="row fv-row mb-7">
<div class="col-md-3 text-md-start">
<label class="fs-6 fw-semibold form-label mt-3">
<TL>Username</TL>
</label>
</div>
<div class="col-md-9">
<input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Username)">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<TL>Close</TL>
</button>
</div>
</div>
</div>
</div>
@code
{
[Parameter]
public FileAccess Access { get; set; }
private string Host = "";
private string Username = "";
private int Port;
protected override async Task OnParametersSetAsync()
{
try
{
Uri uri = new Uri(await Access.GetLaunchUrl());
Host = uri.Host;
Port = uri.Port;
Username = uri.UserInfo.Split(':')[0];
}
catch (NotImplementedException)
{
Host = "N/A";
Port = -1;
Username = "N/A";
}
}
public async Task Show()
{
await ModalService.Show("connectionDetails");
}
}

View File

@@ -26,7 +26,7 @@ else
<div class="card-header border-0 my-2">
<div class="card-title">
<div class="d-flex flex-stack">
<FilePath Access="Access" OnPathChanged="OnComponentStateChanged" />
<FilePath Access="Access" OnPathChanged="OnComponentStateChanged"/>
</div>
</div>
<div class="card-toolbar">
@@ -34,7 +34,9 @@ else
@if (View != null && View.SelectedFiles.Any())
{
<div class="fw-bold me-5">
<span class="me-2">@(View.SelectedFiles.Length) <TL>selected</TL></span>
<span class="me-2">
@(View.SelectedFiles.Length) <TL>selected</TL>
</span>
</div>
<WButton Text="@(SmartTranslateService.Translate("Move"))"
@@ -57,37 +59,46 @@ else
}
else
{
<button type="button" @onclick="Launch" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-muted 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="M5 16C3.3 16 2 14.7 2 13C2 11.3 3.3 10 5 10H5.1C5 9.7 5 9.3 5 9C5 6.2 7.2 4 10 4C11.9 4 13.5 5 14.3 6.5C14.8 6.2 15.4 6 16 6C17.7 6 19 7.3 19 9C19 9.4 18.9 9.7 18.8 10C18.9 10 18.9 10 19 10C20.7 10 22 11.3 22 13C22 14.7 20.7 16 19 16H5ZM8 13.6H16L12.7 10.3C12.3 9.89999 11.7 9.89999 11.3 10.3L8 13.6Z" fill="currentColor"/>
<path d="M11 13.6V19C11 19.6 11.4 20 12 20C12.6 20 13 19.6 13 19V13.6H11Z" fill="currentColor"/>
</svg>
</span>
<div class="btn-group me-3">
<button type="button" @onclick="Launch" class="btn btn-light-primary">
<TL>Launch WinSCP</TL>
</button>
<button type="button" class="btn btn-light-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item btn" target="_blank" href="https://winscp.net/eng/downloads.php">
<TL>Download WinSCP</TL>
</a>
</li>
<li>
<button class="dropdown-item btn" @onclick="() => ConnectionDetailsModal.Show()">
<TL>Show connection details</TL>
</button>
</li>
</ul>
</div>
<button type="button" @onclick="CreateFile" class="btn btn-light-primary me-3">
<span class="svg-icon svg-icon-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path fill="currentColor" d="M6 22h12a2 2 0 0 0 2-2V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2zm7-18 5 5h-5V4zM8 14h3v-3h2v3h3v2h-3v3h-2v-3H8v-2z"></path>
</svg>
</span>
<div class="btn-group me-3">
<button type="button" class="btn btn-light-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<TL>New</TL>&nbsp;
</button>
<ul class="dropdown-menu">
<li>
<button @onclick="CreateFile" class="dropdown-item btn">
<TL>New file</TL>
</button>
<button type="button" @onclick="CreateFolder" class="btn btn-light-primary me-3">
<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.2C9.7 3 10.2 3.20001 10.4 3.60001ZM16 12H13V9C13 8.4 12.6 8 12 8C11.4 8 11 8.4 11 9V12H8C7.4 12 7 12.4 7 13C7 13.6 7.4 14 8 14H11V17C11 17.6 11.4 18 12 18C12.6 18 13 17.6 13 17V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
<path opacity="0.3" d="M11 14H8C7.4 14 7 13.6 7 13C7 12.4 7.4 12 8 12H11V14ZM16 12H13V14H16C16.6 14 17 13.6 17 13C17 12.4 16.6 12 16 12Z" fill="currentColor"></path>
</svg>
</span>
</li>
<li>
<button @onclick="CreateFolder" class="dropdown-item btn">
<TL>New folder</TL>
</button>
</li>
</ul>
</div>
<FileUpload Access="Access" OnUploadComplete="OnComponentStateChanged" />
<FileUpload Access="Access" OnUploadComplete="OnComponentStateChanged"/>
}
</div>
</div>
@@ -110,6 +121,8 @@ else
Access="MoveAccess"
OnSubmit="OnFileMoveSubmit">
</FileSelectModal>
<ConnectionDetailsModal @ref="ConnectionDetailsModal" Access="Access"/>
}
@code
@@ -135,6 +148,9 @@ else
// Config
private ContextAction[] Actions = Array.Empty<ContextAction>();
// Connection details
private ConnectionDetailsModal ConnectionDetailsModal;
protected override void OnInitialized()
{
MoveAccess = (FileAccess)Access.Clone();
@@ -162,7 +178,7 @@ else
}
});
actions.Add(new ()
actions.Add(new()
{
Id = "download",
Name = "Download",
@@ -200,7 +216,7 @@ else
}
});
actions.Add(new ()
actions.Add(new()
{
Id = "decompress",
Name = "Decompress",
@@ -305,7 +321,7 @@ else
if (string.IsNullOrEmpty(name))
return;
await Access.Write(new FileData{IsFile = true, Name = name}, "");
await Access.Write(new FileData { IsFile = true, Name = name }, "");
await View!.Refresh();
}

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

@@ -10,8 +10,8 @@
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/system/logs">
<TL>Logs</TL>
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/system/sentry">
<TL>Sentry</TL>
</a>
</li>
<li class="nav-item mt-2">
@@ -39,6 +39,11 @@
<TL>News</TL>
</a>
</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>
</ul>
</div>
</div>

View File

@@ -8,7 +8,7 @@
<div class="d-flex justify-content-between align-items-start flex-wrap mb-2">
<div class="d-flex flex-column">
<div class="d-flex align-items-center mb-2">
<a class="text-gray-900 fs-2 fw-bold me-1">@(User.FirstName) @(User.LastName)</a>
<a class="text-gray-900 fs-2 fw-bold me-1 @(User.StreamerMode ? "blur" : "")">@(User.FirstName) @(User.LastName)</a>
@if (User.Status == UserStatus.Verified)
{
@@ -16,7 +16,7 @@
}
</div>
<div class="d-flex flex-wrap fw-semibold fs-6 mb-4 pe-2">
<span class="d-flex align-items-center text-gray-400 mb-2">
<span class="d-flex align-items-center text-gray-400 mb-2 @(User.StreamerMode ? "blur" : "")">
@(User.Email)
</span>
</div>

View File

@@ -4,31 +4,31 @@
@{
var marketingConfig = ConfigService
.GetSection("Moonlight")
.GetSection("Marketing");
.Get()
.Moonlight.Marketing;
}
<div id="kt_app_footer" class="app-footer">
<div class="app-container container-fluid d-flex flex-column flex-md-row flex-center flex-md-stack py-3">
<div class="text-dark order-2 order-md-1">
<span class="text-muted fw-semibold me-1">2022 - @DateTime.Now.Year©</span>
<a href="@(marketingConfig.GetValue<string>("Website"))" target="_blank" class="text-gray-800 text-hover-primary">
@(marketingConfig.GetValue<string>("BrandName"))
<a href="@(marketingConfig.Website)" target="_blank" class="text-gray-800 text-hover-primary">
@(marketingConfig.BrandName)
</a>
</div>
<ul class="menu menu-gray-600 menu-hover-primary fw-semibold order-1">
<li class="menu-item">
<a href="@(marketingConfig.GetValue<string>("About"))" target="_blank" class="menu-link px-2">
<a href="@(marketingConfig.About)" target="_blank" class="menu-link px-2">
<TL>About us</TL>
</a>
</li>
<li class="menu-item">
<a href="@(marketingConfig.GetValue<string>("Imprint"))" target="_blank" class="menu-link px-2">
<a href="@(marketingConfig.Imprint)" target="_blank" class="menu-link px-2">
<TL>Imprint</TL>
</a>
</li>
<li class="menu-item">
<a href="@(marketingConfig.GetValue<string>("Privacy"))" target="_blank" class="menu-link px-2">
<a href="@(marketingConfig.Privacy)" target="_blank" class="menu-link px-2">
<TL>Privacy</TL>
</a>
</li>

View File

@@ -69,14 +69,16 @@
</div>
<div class="d-flex flex-column">
<div class="fw-bold d-flex align-items-center fs-5">
<div class="@(User.StreamerMode ? "blur" : "")">
@(User.FirstName) @(User.LastName)
</div>
@if (User.Admin)
{
<span class="badge badge-light-success fw-bold fs-8 px-2 py-1 ms-2">Admin</span>
}
</div>
<a class="fw-semibold text-muted text-hover-primary fs-7">@(User.Email)</a>
<a class="fw-semibold text-muted text-hover-primary fs-7 @(User.StreamerMode ? "blur" : "")">@(User.Email)</a>
</div>
</div>
</div>

View File

@@ -1,10 +1,6 @@
@using Moonlight.App.Services
@using Moonlight.App.Services.Files
@inject ConfigService ConfigService
@{
var moonlightConfig = ConfigService.GetSection("Moonlight");
}
@inject ResourceService ResourceService
<div id="kt_app_header" class="app-header">
<div class="app-container container-fluid d-flex align-items-stretch justify-content-between">
@@ -15,7 +11,7 @@
</div>
<div class="d-flex align-items-center flex-grow-1 flex-lg-grow-0">
<a href="/" class="d-lg-none">
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg" class="h-30px"/>
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="h-30px"/>
</a>
</div>
<div class="d-flex align-items-stretch justify-content-between flex-lg-grow-1" id="kt_app_header_wrapper">

View File

@@ -1,32 +1,28 @@
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Database.Entities
@using Moonlight.App.Services
@using Moonlight.App.Services.Files
@inject IdentityService IdentityService
@inject ConfigService ConfigService
@inject ResourceService ResourceService
@inject IJSRuntime JsRuntime
@{
var moonlightConfig = ConfigService
.GetSection("Moonlight");
}
<div id="kt_app_sidebar" class="app-sidebar flex-column" data-kt-drawer="true" data-kt-drawer-name="app-sidebar" data-kt-drawer-activate="{default: true, lg: false}" data-kt-drawer-overlay="true" data-kt-drawer-width="225px" data-kt-drawer-direction="start" data-kt-drawer-toggle="#kt_app_sidebar_mobile_toggle">
<div class="app-sidebar-logo px-6" id="kt_app_sidebar_logo">
<a href="@(User != null ? "/" : "/login")">
@if (sidebar == "dark-sidebar")
{
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logolong.png" class="h-45px app-sidebar-logo-default"/>
<img alt="Logo" src="@(ResourceService.Image("logolong.png"))" class="h-45px app-sidebar-logo-default"/>
}
else
{
if (sidebar == "light-sidebar")
{
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg" class="theme-light-show h-20px app-sidebar-logo-default"/>
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg" class="theme-dark-show h-20px app-sidebar-logo-default"/>
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="theme-light-show h-20px app-sidebar-logo-default"/>
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="theme-dark-show h-20px app-sidebar-logo-default"/>
}
}
<img alt="Logo" src="@(moonlightConfig.GetValue<string>("AppUrl"))/api/moonlight/resources/images/logo.svg" class="h-20px app-sidebar-logo-minimize"/>
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))" class="h-20px app-sidebar-logo-minimize"/>
</a>
<div id="kt_app_sidebar_toggle" class="app-sidebar-toggle btn btn-icon btn-shadow btn-sm btn-color-muted btn-active-color-primary body-bg h-30px w-30px position-absolute top-50 start-100 translate-middle rotate" data-kt-toggle="true" data-kt-toggle-state="active" data-kt-toggle-target="body" data-kt-toggle-name="app-sidebar-minimize">
@@ -38,7 +34,7 @@
<div class="app-sidebar-footer flex-column-auto pt-2 pb-6 px-6" id="kt_app_sidebar_footer">
<a href="/support" class="btn btn-flex flex-center btn-custom btn-primary overflow-hidden text-nowrap px-0 h-40px w-100 btn-label">
<TL>Open support</TL>
<i class="bx bx-sm bx-support"></i>
</a>
</div>
</div>

View File

@@ -1,20 +0,0 @@
@using Moonlight.App.Services
@inject ConfigService ConfigService
@{
var setupComplete = ConfigService
.GetSection("Moonlight")
.GetValue<bool>("SetupComplete");
}
@if (!setupComplete)
{
@ChildContent
}
@code
{
[Parameter]
public RenderFragment ChildContent { get; set; }
}

View File

@@ -1,20 +0,0 @@
@using Moonlight.App.Services
@inject ConfigService ConfigService
@{
var setupComplete = ConfigService
.GetSection("Moonlight")
.GetValue<bool>("SetupComplete");
}
@if (setupComplete)
{
@ChildContent
}
@code
{
[Parameter]
public RenderFragment ChildContent { get; set; }
}

View File

@@ -38,9 +38,17 @@
}
public async void Dispose()
{
try
{
await Xterm.DisposeAsync();
}
catch (Exception)
{
// ignore dispose errors. They occur when the tab closes unexpectedly
// so we can ignore them
}
}
private async void OnFirstRender()
{

View File

@@ -14,7 +14,7 @@
@inject IJSRuntime JsRuntime
@inject IdentityService IdentityService
@inject SessionService SessionService
@inject SessionClientService SessionClientService
@inject NavigationManager NavigationManager
@inject EventSystem Event
@inject ToastService ToastService
@@ -163,13 +163,11 @@
private bool IsIpBanned = false;
protected override void OnInitialized()
{
AddBodyAttribute("data-kt-app-page-loading", "on");
}
protected override void OnAfterRender(bool firstRender)
{
if(firstRender)
AddBodyAttribute("data-kt-app-page-loading", "on");
//Initialize classes and attributes for layout with dark sidebar
AddBodyAttribute("data-kt-app-reset-transition", "true");
@@ -217,12 +215,10 @@
}
catch (Exception){ /* ignore errors to make sure that the session call is executed */ }
await SessionService.Register();
await SessionClientService.Start();
NavigationManager.LocationChanged += async (_, _) =>
{
SessionService.Refresh();
if (!NavigationManager.Uri.Contains("/server/"))
await DynamicBackgroundService.Reset();
};
@@ -257,7 +253,7 @@
public async void Dispose()
{
SessionService.Close();
await SessionClientService.Stop();
await KeyListenerService.DisposeAsync();

View File

@@ -9,7 +9,7 @@
@inject IJSRuntime JsRuntime
@inject IdentityService IdentityService
@inject SessionService SessionService
@inject SessionClientService SessionClientService
@inject NavigationManager NavigationManager
<GlobalErrorBoundary>
@@ -106,9 +106,7 @@
await JsRuntime.InvokeVoidAsync("KTDrawer.createInstances");
await JsRuntime.InvokeVoidAsync("createSnow");
await SessionService.Register();
NavigationManager.LocationChanged += (sender, args) => { SessionService.Refresh(); };
await SessionClientService.Start();
}
catch (Exception)
{
@@ -117,9 +115,9 @@
}
}
public void Dispose()
public async void Dispose()
{
SessionService.Close();
await SessionClientService.Stop();
}
private void AddBodyAttribute(string attribute, string value)

View File

@@ -1,4 +1,5 @@
@inherits LayoutComponentBase
@using Moonlight.App.Extensions
@inherits LayoutComponentBase
@inject IJSRuntime JS
@inject NavigationManager NavigationManager
@@ -14,23 +15,23 @@ window.emptyBody = function(){
{
protected override void OnAfterRender(bool firstRender)
{
JS.InvokeVoidAsync("KTThemeMode.init");
JS.InvokeVoidAsync("emptyBody");
JS.InvokeVoidSafe("KTThemeMode.init");
JS.InvokeVoidSafe("emptyBody");
if (firstRender)
{
JS.InvokeVoidAsync("scrollTo", 0, 0);
JS.InvokeVoidAsync("KTDialer.init");
JS.InvokeVoidAsync("KTDrawer.init");
JS.InvokeVoidAsync("KTMenu.init");
JS.InvokeVoidAsync("KTImageInput.init");
JS.InvokeVoidAsync("KTPasswordMeter.init");
JS.InvokeVoidAsync("KTScroll.init");
JS.InvokeVoidAsync("KTScrolltop.init");
JS.InvokeVoidAsync("KTSticky.init");
JS.InvokeVoidAsync("KTSwapper.init");
JS.InvokeVoidAsync("KTToggle.init");
JS.InvokeVoidAsync("KTMenu.updateByLinkAttribute", $"/{NavigationManager.ToBaseRelativePath(NavigationManager.Uri)}");
JS.InvokeVoidSafe("scrollTo", 0, 0);
JS.InvokeVoidSafe("KTDialer.init");
JS.InvokeVoidSafe("KTDrawer.init");
JS.InvokeVoidSafe("KTMenu.init");
JS.InvokeVoidSafe("KTImageInput.init");
JS.InvokeVoidSafe("KTPasswordMeter.init");
JS.InvokeVoidSafe("KTScroll.init");
JS.InvokeVoidSafe("KTScrolltop.init");
JS.InvokeVoidSafe("KTSticky.init");
JS.InvokeVoidSafe("KTSwapper.init");
JS.InvokeVoidSafe("KTToggle.init");
JS.InvokeVoidSafe("KTMenu.updateByLinkAttribute", $"/{NavigationManager.ToBaseRelativePath(NavigationManager.Uri)}");
}
JS.InvokeVoidAsync("KTLayoutSearch.init");
@@ -45,30 +46,18 @@ window.emptyBody = function(){
private async void OnLocationChanged(object sender, LocationChangedEventArgs args)
{
await InvokeJsSave("scrollTo", 0, 0);
await InvokeJsSave("KTDrawer.createInstances");
await InvokeJsSave("KTMenu.createInstances");
await InvokeJsSave("KTImageInput.createInstances");
await InvokeJsSave("KTPasswordMeter.createInstances");
await InvokeJsSave("KTScroll.createInstances");
await InvokeJsSave("KTScrolltop.createInstances");
await InvokeJsSave("KTSticky.createInstances");
await InvokeJsSave("KTSwapper.createInstances");
await InvokeJsSave("KTToggle.createInstances");
await InvokeJsSave("KTMenu.updateByLinkAttribute", $"/{NavigationManager.ToBaseRelativePath(args.Location)}");
await InvokeJsSave("KTAppSidebar.init");
}
private async Task InvokeJsSave(string method, params object[] args)
{
try
{
await JS.InvokeVoidAsync(method, args);
}
catch (Exception)
{
// ignored
}
await JS.InvokeVoidSafeAsync("scrollTo", 0, 0);
await JS.InvokeVoidSafeAsync("KTDrawer.createInstances");
await JS.InvokeVoidSafeAsync("KTMenu.createInstances");
await JS.InvokeVoidSafeAsync("KTImageInput.createInstances");
await JS.InvokeVoidSafeAsync("KTPasswordMeter.createInstances");
await JS.InvokeVoidSafeAsync("KTScroll.createInstances");
await JS.InvokeVoidSafeAsync("KTScrolltop.createInstances");
await JS.InvokeVoidSafeAsync("KTSticky.createInstances");
await JS.InvokeVoidSafeAsync("KTSwapper.createInstances");
await JS.InvokeVoidSafeAsync("KTToggle.createInstances");
await JS.InvokeVoidSafeAsync("KTMenu.updateByLinkAttribute", $"/{NavigationManager.ToBaseRelativePath(args.Location)}");
await JS.InvokeVoidSafeAsync("KTAppSidebar.init");
}
public void Dispose()

View File

@@ -151,8 +151,8 @@
await lazyLoader.SetText("Loading health check data");
var appUrl = ConfigService
.GetSection("Moonlight")
.GetValue<string>("AppUrl");
.Get()
.Moonlight.AppUrl;
try
{

View File

@@ -4,12 +4,10 @@
@using Moonlight.App.Services
@inject NodeRepository NodeRepository
@inject SmartTranslateService SmartTranslateService
@inject ConfigService ConfigService
@inject NavigationManager NavigationManager
@{
var appUrl = ConfigService.GetSection("Moonlight").GetValue<string>("AppUrl");
var appUrl = ConfigService.Get().Moonlight.AppUrl;
}
<OnlyAdmin>
@@ -42,7 +40,7 @@
&nbsp;&nbsp;host: 0.0.0.0<br/>
&nbsp;&nbsp;port: @(Node.HttpPort)<br/>
&nbsp;&nbsp;ssl:<br/>
&nbsp;&nbsp;&nbsp;&nbsp;enabled: false<br/>
&nbsp;&nbsp;&nbsp;&nbsp;enabled: @(Node.Ssl ? "true" : "false")<br/>
&nbsp;&nbsp;&nbsp;&nbsp;cert: /etc/letsencrypt/live/@(Node.Fqdn)/fullchain.pem<br/>
&nbsp;&nbsp;&nbsp;&nbsp;key: /etc/letsencrypt/live/@(Node.Fqdn)/privkey.pem<br/>
&nbsp;&nbsp;disable_remote_download: false<br/>

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

@@ -1,20 +0,0 @@
@page "/admin/system/logs"
@using BlazorTable
@using Moonlight.App.Models.Misc
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Navigations
@inject SmartTranslateService SmartTranslateService
<OnlyAdmin>
<AdminSystemNavigation Index="1"/>
</OnlyAdmin>
@code
{
private Task Load(LazyLoader arg)
{
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,40 @@
@page "/admin/system/sentry"
@using Moonlight.Shared.Components.Navigations
@using global::Sentry
<OnlyAdmin>
<AdminSystemNavigation Index="1"/>
<div class="row">
<div class="col-xxl-6 my-3">
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Status</TL>
</span>
</div>
<div class="card-body">
<span class="fs-5">
@if (SentrySdk.IsEnabled)
{
<TL>Sentry is enabled</TL>
}
else
{
<TL>Sentry is disabled</TL>
}
</span>
</div>
</div>
</div>
</div>
</OnlyAdmin>
@code
{
private Task Load(LazyLoader arg)
{
return Task.CompletedTask;
}
}

View File

@@ -8,7 +8,7 @@
@inject UserRepository UserRepository
@inject UserService UserService
@inject SessionService SessionService
@inject SessionServerService SessionServerService
@inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService
@@ -174,7 +174,7 @@
user.Status = User!.Status;
UserRepository.Update(user);
SessionService.ReloadUserSessions(User);
await SessionServerService.ReloadUserSessions(User);
await ToastService.Success(SmartTranslateService.Translate("Successfully updated user"));
}
@@ -191,7 +191,7 @@
await UserService.ChangePassword(User!, NewPassword, true);
NewPassword = "";
SessionService.ReloadUserSessions(User);
await SessionServerService.ReloadUserSessions(User!);
await ToastService.Success(SmartTranslateService.Translate("Successfully updated password"));
}

View File

@@ -8,9 +8,10 @@
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@inject SessionService SessionService
@inject SessionServerService SessionServerService
@inject SmartTranslateService SmartTranslateService
@inject AlertService AlertService
@inject ToastService ToastService
<OnlyAdmin>
<AdminSessionNavigation Index="1"/>
@@ -44,8 +45,8 @@
}
else
{
<Table TableItem="Session" Items="AllSessions" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("Email"))" Field="@(x => x.User.Email)" Sortable="true" Filterable="true" Width="20%">
<Table TableItem="SessionClientService" Items="AllSessions" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("Email"))" Field="@(x => x.User.Email)" Sortable="true" Filterable="true" Width="20%">
<Template>
@if (context.User == null)
{
@@ -57,25 +58,33 @@
}
</Template>
</Column>
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("IP"))" Field="@(x => x.Ip)" Sortable="true" Filterable="true" Width="10%"/>
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("URL"))" Field="@(x => x.Url)" Sortable="true" Filterable="true" Width="10%"/>
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("Device"))" Field="@(x => x.Device)" Sortable="true" Filterable="true" Width="10%"/>
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("Time"))" Field="@(x => x.CreatedAt)" Sortable="true" Filterable="true" Width="10%">
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("IP"))" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%">
<Template>
@(context.IdentityService.GetIp())
</Template>
</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("Device"))" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%">
<Template>
@(context.IdentityService.GetDevice())
</Template>
</Column>
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("Time"))" Field="@(x => x.CreateTimestamp)" Sortable="true" Filterable="true" Width="10%">
<Template>
@{
var time = Formatter.FormatUptime((DateTime.UtcNow - context.CreatedAt).TotalMilliseconds);
var time = Formatter.FormatUptime((DateTime.UtcNow - context.CreateTimestamp).TotalMilliseconds);
}
<span>@(time)</span>
</Template>
</Column>
<Column TableItem="Session" Title="@(SmartTranslateService.Translate("Actions"))" Field="@(x => x.Ip)" Sortable="false" Filterable="false" Width="10%">
<Column TableItem="SessionClientService" Title="@(SmartTranslateService.Translate("Actions"))" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%">
<Template>
<button @onclick="() => Navigate(context)" class="btn btn-sm btn-primary">
<TL>Change url</TL>
</button>
</Template>
</Column>
<Column TableItem="Session" Title="" Field="@(x => x.Ip)" Sortable="false" Filterable="false" Width="10%">
<Column TableItem="SessionClientService" Title="" Field="@(x => x.Uuid)" Sortable="false" Filterable="false" Width="10%">
<Template>
<button @onclick="() => Message(context)" class="btn btn-sm btn-warning">
<TL>Message</TL>
@@ -92,11 +101,11 @@
@code
{
private Session[]? AllSessions;
private SessionClientService[]? AllSessions;
private Task Load(LazyLoader arg)
private async Task Load(LazyLoader arg)
{
AllSessions = SessionService.GetAll();
AllSessions = await SessionServerService.GetSessions();
Task.Run(async () =>
{
@@ -109,35 +118,30 @@
}
catch (Exception e)
{
Logger.Warn("Error autorefreshing sessions");
Logger.Warn("Error auto refreshing sessions");
Logger.Warn(e);
}
}
});
return Task.CompletedTask;
}
private async Task Refresh()
{
AllSessions = SessionService.GetAll();
AllSessions = await SessionServerService.GetSessions();
await InvokeAsync(StateHasChanged);
}
private async Task Navigate(Session session)
private async Task Navigate(SessionClientService session)
{
var url = await AlertService.Text("URL", SmartTranslateService.Translate("Enter url"), "");
if (url == null)
return;
if (url == "")
if (string.IsNullOrEmpty(url))
return;
if (url == "null")
return;
session.Navigation.NavigateTo(url, true);
session.NavigationManager.NavigateTo(url, true);
}
private async Task MessageAll()
@@ -157,7 +161,9 @@
if (b)
{
foreach (var session in SessionService.GetAll())
foreach (var session in AllSessions!)
{
Task.Run(async () =>
{
try
{
@@ -167,12 +173,18 @@
{
Logger.Warn("Error sending user a alert");
Logger.Warn(e);
var translation = SmartTranslateService.Translate("An unknown error occured while sending admin message to user: ");
var identifier = session.User != null ? session.User.Email : session.Uuid.ToString();
await ToastService.Warning(translation + identifier);
}
});
}
}
}
private async Task Message(Session session)
private async Task Message(SessionClientService session)
{
var message = await AlertService.Text(
SmartTranslateService.Translate("Enter message"),
@@ -191,12 +203,14 @@
{
try
{
await session.AlertService.Warning("Admin Message", message);
await session.AlertService.Warning("Admin Message", message!);
}
catch (Exception e)
{
Logger.Warn("Error sending user a alert");
Logger.Warn(e);
await ToastService.Warning(SmartTranslateService.Translate("An unknown error occured while sending admin message"));
}
}
}

View File

@@ -29,9 +29,15 @@ else
</div>
<div class="card card-body mb-5">
<div class="btn-group">
<a class="btn btn-primary" href="/admin/users/edit/@(User.Id)"><TL>Edit</TL></a>
<a class="btn btn-secondary" href="/admin/users"><TL>Back to list</TL></a>
<a class="btn btn-primary" href="/admin/support/view/@(User.Id)"><TL>Open support</TL></a>
<a class="btn btn-primary" href="/admin/users/edit/@(User.Id)">
<TL>Edit</TL>
</a>
<a class="btn btn-secondary" href="/admin/users">
<TL>Back to list</TL>
</a>
<a class="btn btn-primary" href="/admin/support/view/@(User.Id)">
<TL>Open support</TL>
</a>
</div>
</div>
<div class="card card-xl-stretch mb-5">
@@ -209,6 +215,24 @@ else
<span class="fw-bold fs-6 text-gray-800">@(Formatter.FormatDate(User.CreatedAt))</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Register ip</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.RegisterIp)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Last ip</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.LastIp)</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -5,6 +5,8 @@
@using Moonlight.App.Models.Forms
@using Moonlight.App.Repositories
@using Mappy.Net
@using Moonlight.App.Exceptions
@using Moonlight.App.Helpers
@inject UserRepository UserRepository
@@ -13,7 +15,7 @@
<LazyLoader Load="Load">
<SmartForm OnValidSubmit="Save" Model="Model">
<div class="card mb-5 mb-xl-10">
<div class="card-body p-9">
<div class="card-body p-9 @(CurrentUser.StreamerMode ? "blur" : "")">
<div class="row">
<div class="col-lg-6 fv-row fv-plugins-icon-container">
<div class="mb-3">
@@ -74,7 +76,7 @@
@code
{
private UserDataModel Model = new UserDataModel();
private UserDataModel Model = new();
[CascadingParameter]
public User CurrentUser { get; set; }
@@ -89,9 +91,20 @@
private Task Save()
{
CurrentUser = Mapper.Map(CurrentUser, Model);
// Prevent users from locking out other users by changing their email
CurrentUser.Email = CurrentUser.Email.ToLower();
Model.Email = Model.Email.ToLower();
var userWithThatEmail = UserRepository
.Get()
.FirstOrDefault(x => x.Email == Model.Email);
if (userWithThatEmail != null && CurrentUser.Id != userWithThatEmail.Id)
{
Logger.Warn($"A user tried to lock another user out by changing the email. Email: {Model.Email}", "security");
throw new DisplayException("A user with that email does already exist");
}
CurrentUser = Mapper.Map(CurrentUser, Model);
UserRepository.Update(CurrentUser);

View File

@@ -2,130 +2,152 @@
@using Moonlight.Shared.Components.Navigations
@using QRCoder
@using Moonlight.App.Services.Sessions
@using Moonlight.App.Services
@using Moonlight.App.Services.Interop
@using System.Text.RegularExpressions
@using Moonlight.App.Database.Entities
@using Moonlight.App.Models.Misc
@using Mappy.Net
@using Moonlight.App.Models.Forms
@using Moonlight.App.Repositories
@inject SmartTranslateService SmartTranslateService
@inject TotpService TotpService
@inject NavigationManager NavigationManager
@inject IdentityService IdentityService
@inject UserService UserService
@inject AlertService AlertService
@inject ToastService ToastService
@inject NavigationManager NavigationManager
@inject ModalService ModalService
@inject Repository<User> UserRepository
@inject SmartTranslateService SmartTranslateService
<ProfileNavigation Index="2"/>
<div class="card mb-5 mb-xl-10">
<LazyLoader Load="Load">
@if (TotpEnabled)
{
<div class="alert alert-primary d-flex rounded ms-6 me-6 mt-6 mb-8">
<table class="w-100">
<tr>
<td rowspan="2">
<span class="svg-icon svg-icon-2tx svg-icon-primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M20.5543 4.37824L12.1798 2.02473C12.0626 1.99176 11.9376 1.99176 11.8203 2.02473L3.44572 4.37824C3.18118 4.45258 3 4.6807 3 4.93945V13.569C3 14.6914 3.48509 15.8404 4.4417 16.984C5.17231 17.8575 6.18314 18.7345 7.446 19.5909C9.56752 21.0295 11.6566 21.912 11.7445 21.9488C11.8258 21.9829 11.9129 22 12.0001 22C12.0872 22 12.1744 21.983 12.2557 21.9488C12.3435 21.912 14.4326 21.0295 16.5541 19.5909C17.8169 18.7345 18.8277 17.8575 19.5584 16.984C20.515 15.8404 21 14.6914 21 13.569V4.93945C21 4.6807 20.8189 4.45258 20.5543 4.37824Z" fill="currentColor"></path>
<path d="M10.5606 11.3042L9.57283 10.3018C9.28174 10.0065 8.80522 10.0065 8.51412 10.3018C8.22897 10.5912 8.22897 11.0559 8.51412 11.3452L10.4182 13.2773C10.8099 13.6747 11.451 13.6747 11.8427 13.2773L15.4859 9.58051C15.771 9.29117 15.771 8.82648 15.4859 8.53714C15.1948 8.24176 14.7183 8.24176 14.4272 8.53714L11.7002 11.3042C11.3869 11.6221 10.874 11.6221 10.5606 11.3042Z" fill="currentColor"></path>
</svg>
</span>
</td>
<td class="w-100">
<h4 class="text-gray-900 fw-bold ms-4">
<TL>Your account is secured with 2fa</TL>
</h4>
</td>
<td rowspan="2">
<a @onclick="Disable" class="btn btn-primary px-6 align-self-center text-nowrap" data-bs-toggle="modal" data-bs-target="#twofactorauth">
<TL>Disable</TL>
</a>
</td>
</tr>
<tr>
<td>
<div class="fs-6 text-gray-700 pe-7 ms-4">
<TL>anyone write a fancy text here?</TL>
<div class="row">
<div class="col-12 col-md-6 p-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Two factor authentication</TL>
</div>
</td>
</tr>
</table>
</div>
<div class="card-body fs-6">
<p>
<TL>2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.</TL>
</p>
<div class="d-flex justify-content-center">
@if (User.TotpEnabled)
{
<WButton Text="@(SmartTranslateService.Translate("Disable"))"
WorkingText=""
CssClasses="btn-danger"
OnClick="DisableTwoFactor">
</WButton>
}
else
{
<WButton Text="@(SmartTranslateService.Translate("Enable"))"
WorkingText=""
CssClasses="btn-primary"
OnClick="StartTwoFactorWizard">
</WButton>
}
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 p-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Password</TL>
</div>
</div>
<div class="card-body fs-6">
<div class="d-flex justify-content-center">
<div class="input-group">
<input @bind="Password" class="form-control" type="password"/>
<WButton Text="@(SmartTranslateService.Translate("Enable"))"
WorkingText=""
CssClasses="btn-primary"
OnClick="ChangePassword">
</WButton>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6 p-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<TL>Preferences</TL>
</div>
</div>
<div class="card-body fs-6">
<div class="form-check form-switch">
<input @bind="UserModel.StreamerMode" class="form-check-input" type="checkbox" role="switch" id="streamerModeSwitch">
<label class="form-check-label" for="streamerModeSwitch">
<TL>Streamer mode</TL>
</label>
</div>
</div>
<div class="card-footer">
<div class="text-end">
<WButton Text="@(SmartTranslateService.Translate("Save"))"
WorkingText=""
CssClasses="btn-primary"
OnClick="SavePreferences">
</WButton>
</div>
</div>
</div>
</div>
</div>
@* Modals *@
<div class="modal fade" id="2fa" tabindex="-1" style="display: none" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">
<TL>Activate 2fa</TL>
</h2>
</div>
<div class="modal-body fs-6">
@if (!User.TotpEnabled)
{
if (string.IsNullOrEmpty(User.TotpSecret))
{
<p>
<TL>Make sure you have installed one of the following apps on your smartphone and press continue</TL>
</p>
<a href="https://support.google.com/accounts/answer/1066447?hl=en" target="_blank">Google Authenticator</a>
<br/>
<a href="https://www.microsoft.com/en-us/account/authenticator" target="_blank">Microsoft Authenticator</a>
<br/>
<a href="https://authy.com/download/" target="_blank">Authy</a>
<br/>
<a href="https://support.1password.com/one-time-passwords/" target="_blank">1Password</a>
<br/>
<div class="d-flex justify-content-center">
<WButton Text="@(SmartTranslateService.Translate("Continue"))"
WorkingText="@(SmartTranslateService.Translate("Preparing"))"
CssClasses="btn-primary"
OnClick="GenerateTwoFactorToken">
</WButton>
</div>
}
else
{
<div class="alert alert-primary d-flex rounded ms-6 me-6 mt-6 mb-8">
<table class="w-100">
<tr>
<td rowspan="2">
<span class="svg-icon svg-icon-2tx svg-icon-primary">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M20.5543 4.37824L12.1798 2.02473C12.0626 1.99176 11.9376 1.99176 11.8203 2.02473L3.44572 4.37824C3.18118 4.45258 3 4.6807 3 4.93945V13.569C3 14.6914 3.48509 15.8404 4.4417 16.984C5.17231 17.8575 6.18314 18.7345 7.446 19.5909C9.56752 21.0295 11.6566 21.912 11.7445 21.9488C11.8258 21.9829 11.9129 22 12.0001 22C12.0872 22 12.1744 21.983 12.2557 21.9488C12.3435 21.912 14.4326 21.0295 16.5541 19.5909C17.8169 18.7345 18.8277 17.8575 19.5584 16.984C20.515 15.8404 21 14.6914 21 13.569V4.93945C21 4.6807 20.8189 4.45258 20.5543 4.37824Z" fill="currentColor"></path>
<path d="M10.5606 11.3042L9.57283 10.3018C9.28174 10.0065 8.80522 10.0065 8.51412 10.3018C8.22897 10.5912 8.22897 11.0559 8.51412 11.3452L10.4182 13.2773C10.8099 13.6747 11.451 13.6747 11.8427 13.2773L15.4859 9.58051C15.771 9.29117 15.771 8.82648 15.4859 8.53714C15.1948 8.24176 14.7183 8.24176 14.4272 8.53714L11.7002 11.3042C11.3869 11.6221 10.874 11.6221 10.5606 11.3042Z" fill="currentColor"></path>
</svg>
</span>
</td>
<td class="w-100">
<h4 class="text-gray-900 fw-bold ms-4">
<TL>Secure your account</TL>
</h4>
</td>
<td rowspan="2">
<a @onclick="Enable" class="btn btn-primary px-6 align-self-center text-nowrap" data-bs-toggle="modal" data-bs-target="#twofactorauth">
<TL>Enable</TL>
</a>
</td>
</tr>
<tr>
<td>
<div class="fs-6 text-gray-700 pe-7 ms-4">
<TL>2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.</TL>
</div>
</td>
</tr>
</table>
</div>
}
<div class="modal fade" id="twofactorauth" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered mw-650px">
<div class="modal-content">
<div class="modal-header flex-stack py-6">
<h2 class="ms-3">
<TL>Activate 2fa</TL>
</h2>
<div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
<span class="svg-icon svg-icon-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="6" y="17.3137" width="16" height="2" rx="1" transform="rotate(-45 6 17.3137)" fill="currentColor"></rect>
<rect x="7.41422" y="6" width="16" height="2" rx="1" transform="rotate(45 7.41422 6)" fill="currentColor"></rect>
</svg>
</span>
</div>
</div>
<div class="modal-body scroll-y ps-10 pe-10 pb-10">
<div>
<h3 class="text-dark fw-bold mb-3 mt-2">
<TL>2fa apps</TL>
</h3>
<div class="text-gray-500 fw-semibold fs-6 mb-10">
<TL>Use an app like </TL>
<a href="https://support.google.com/accounts/answer/1066447?hl=en" target="_blank">Google Authenticator</a>,
<a href="https://www.microsoft.com/en-us/account/authenticator" target="_blank">Microsoft Authenticator</a>,
<a href="https://authy.com/download/" target="_blank">Authy</a>, <TL>or</TL>
<a href="https://support.1password.com/one-time-passwords/" target="_blank">1Password</a> <TL>and scan the following QR Code</TL>
@if (EnablingTotp)
{
<div class="pt-5 text-center">
<p>
<TL>Scan the qr code and enter the code generated by the app you have scanned it in</TL>
</p>
<div class="mt-3 text-center">
@{
QRCodeGenerator qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode
(
$"otpauth://totp/{Uri.EscapeDataString(User.Email)}?secret={TotpSecret}&issuer={Uri.EscapeDataString(Issuer)}",
$"otpauth://totp/{Uri.EscapeDataString(User.Email)}?secret={User.TotpSecret}&issuer={Uri.EscapeDataString("Moonlight")}",
QRCodeGenerator.ECCLevel.Q
);
@@ -135,184 +157,77 @@
}
<img src="data:image/png;base64,@(base64)" alt="" class="mw-200px mt-2">
</div>
<div class="mt-3 d-flex justify-content-center">
<div class="input-group">
<input type="text"
@bind="TwoFactorCode"
placeholder="@(SmartTranslateService.Translate("Enter your 2fa code here"))"
class="form-control"/>
<WButton Text="@(SmartTranslateService.Translate("Enable"))"
WorkingText="@(SmartTranslateService.Translate("Processing"))"
CssClasses="btn-primary"
OnClick="EnableTwoFactor">
</WButton>
</div>
</div>
}
}
</div>
<div class="notice d-flex bg-light-warning rounded border-warning border border-dashed mb-8 p-6">
<span class="svg-icon svg-icon-2tx svg-icon-warning me-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.3" x="2" y="2" width="20" height="20" rx="10" fill="currentColor"></rect>
<rect x="11" y="14" width="7" height="2" rx="1" transform="rotate(-90 11 14)" fill="currentColor"></rect>
<rect x="11" y="17" width="2" height="2" rx="1" transform="rotate(-90 11 17)" fill="currentColor"></rect>
</svg>
</span>
<div class="d-flex flex-stack flex-grow-1">
<div class="fw-semibold">
<div class="fs-6 text-gray-700">
<TL>If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:</TL>
<div class="fw-bold text-dark pt-2">@(TotpSecret)</div>
</div>
</div>
</div>
</div>
<a class="btn btn-primary px-6 align-self-center text-nowrap float-end" data-bs-toggle="modal" data-bs-target="#test">
<TL>Next</TL>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="test" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered mw-650px">
<div class="modal-content">
<div class="modal-header flex-stack py-6">
<h2 class="ms-3">
<TL>Finish activation</TL>
</h2>
<div class="btn btn-sm btn-icon btn-active-color-primary" data-bs-dismiss="modal">
<span class="svg-icon svg-icon-1">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.5" x="6" y="17.3137" width="16" height="2" rx="1" transform="rotate(-45 6 17.3137)" fill="currentColor"></rect>
<rect x="7.41422" y="6" width="16" height="2" rx="1" transform="rotate(45 7.41422 6)" fill="currentColor"></rect>
</svg>
</span>
</div>
</div>
<div class="modal-body scroll-y ps-10 pe-10 pb-10">
<div class="text-gray-500 fw-semibold fs-6 mb-10">
<div class="alert alert-primary d-flex align-items-center p-5 mb-6">
<i class="bx bx-info-circle fs-2hx text-primary me-4">
<span class="path1"></span><span class="path2"></span>
</i>
<div class="d-flex flex-column">
<h4 class="mb-1 text-primary">
<TL>2fa Code requiered</TL>
</h4>
<span>In order to finish the activation of 2fa, you need to enter the code your 2fa app shows you.</span>
</div>
</div>
<input type="text" class="form-control form-control-lg form-control-solid mb-0" placeholder="@SmartTranslateService.Translate("2fa Code")" @bind="currentTotp"/>
<br/>
<WButton CssClasses="btn btn-primary mb-2 align-self-center text-nowrap float-end" WorkingText="@SmartTranslateService.Translate("Saving")" Text="@SmartTranslateService.Translate("Finish")" OnClick="CheckAndSaveTotp">
</WButton>
</div>
</div>
</div>
</div>
</div>
<div class="separator mt-2"></div>
<div class="alert alert-danger d-flex rounded ms-6 me-6 mt-6 mb-8 bg-body">
<div class="w-100">
<table>
<tr>
<td>
<span class="svg-icon svg-icon-2tx svg-icon-body">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.3" d="M20.5543 4.37824L12.1798 2.02473C12.0626 1.99176 11.9376 1.99176 11.8203 2.02473L3.44572 4.37824C3.18118 4.45258 3 4.6807 3 4.93945V13.569C3 14.6914 3.48509 15.8404 4.4417 16.984C5.17231 17.8575 6.18314 18.7345 7.446 19.5909C9.56752 21.0295 11.6566 21.912 11.7445 21.9488C11.8258 21.9829 11.9129 22 12.0001 22C12.0872 22 12.1744 21.983 12.2557 21.9488C12.3435 21.912 14.4326 21.0295 16.5541 19.5909C17.8169 18.7345 18.8277 17.8575 19.5584 16.984C20.515 15.8404 21 14.6914 21 13.569V4.93945C21 4.6807 20.8189 4.45258 20.5543 4.37824Z" fill="currentColor"></path>
<path d="M10.5606 11.3042L9.57283 10.3018C9.28174 10.0065 8.80522 10.0065 8.51412 10.3018C8.22897 10.5912 8.22897 11.0559 8.51412 11.3452L10.4182 13.2773C10.8099 13.6747 11.451 13.6747 11.8427 13.2773L15.4859 9.58051C15.771 9.29117 15.771 8.82648 15.4859 8.53714C15.1948 8.24176 14.7183 8.24176 14.4272 8.53714L11.7002 11.3042C11.3869 11.6221 10.874 11.6221 10.5606 11.3042Z" fill="currentColor"></path>
</svg>
</span>
</td>
<td class="w-25">
<span class="text-gray-700 fw-semibold fs-6 ms-4 me-2">
<TL>New password</TL>
</span>
</td>
<td class="w-75">
<input @bind="Password" type="password" class="form-control">
</td>
<td class="">
<WButton OnClick="ChangePassword"
CssClasses="btn-danger ms-4"
Text="@SmartTranslateService.Translate("Change")"
WorkingText="@SmartTranslateService.Translate("Changing")">
</WButton>
</td>
</tr>
</table>
</div>
</div>
</LazyLoader>
</div>
@code
{
private bool TotpEnabled = false;
private bool EnablingTotp = false;
private string TotpSecret = "";
private User User;
private string Issuer = "Moonlight";
private string currentTotp = "";
[CascadingParameter]
public User User { get; set; }
private string TwoFactorCode = "";
private string Password = "";
private UserPreferencesDataModel UserModel;
private async void Enable()
protected override void OnParametersSet()
{
//TODO: AuditLog
await TotpService.Enable();
TotpEnabled = await TotpService.GetEnabled();
TotpSecret = await TotpService.GetSecret();
EnablingTotp = true;
StateHasChanged();
UserModel = Mapper.Map<UserPreferencesDataModel>(User);
}
public async Task CheckAndSaveTotp()
private async Task StartTwoFactorWizard()
{
if (await TotpService.Verify(TotpSecret, currentTotp))
{
await TotpService.EnforceTotpLogin();
TotpEnabled = true;
TotpSecret = await TotpService.GetSecret();
await ToastService.Success("Successfully enabled 2fa!");
StateHasChanged();
}
else
{
await AlertService.Error("2fa code incorrect", "The given 2fa code is incorrect. Maybe check if the code in your 2fa app has changed.");
}
await ModalService.Show("2fa");
}
private async void Disable()
private async Task GenerateTwoFactorToken()
{
//TODO: AuditLog
await TotpService.Disable();
await TotpService.GenerateSecret();
await InvokeAsync(StateHasChanged);
}
private async Task EnableTwoFactor()
{
await ModalService.Hide("2fa");
await TotpService.Enable(TwoFactorCode);
await InvokeAsync(StateHasChanged);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
private async Task Load(LazyLoader lazyLoader)
private async Task DisableTwoFactor()
{
await lazyLoader.SetText("Requesting secrets");
TotpEnabled = await TotpService.GetEnabled();
TotpSecret = await TotpService.GetSecret();
await lazyLoader.SetText("Requesting identity");
User = await IdentityService.Get();
await TotpService.Disable();
await InvokeAsync(StateHasChanged);
}
private async Task ChangePassword()
{
if (Regex.IsMatch(Password, @"^(?=.*[A-Za-z])(?=.*\d)[A-Za-z@$!%*#.,?&\d]{8,}$"))
{
await UserService.ChangePassword(User, Password);
//TODO: AuditLog
// Reload to make the user login again
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
else
private async Task SavePreferences()
{
await AlertService.Error("Error", "Your password must be at least 8 characters and must contain a number");
}
User = Mapper.Map(User, UserModel);
UserRepository.Update(User);
await InvokeAsync(StateHasChanged);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
}

View File

@@ -24,12 +24,11 @@
@if (Subscription == null)
{
var config = ConfigService
.GetSection("Moonlight")
.GetSection("Subscriptions")
.GetSection("Sellpass");
.Get()
.Moonlight.Subscriptions.SellPass;
var enableSellpass = config.GetValue<bool>("Enable");
var url = config.GetValue<string>("Url");
var enableSellpass = config.Enable;
var url = config.Url;
<h3 class="mb-2">
<div class="input-group mb-3">

View File

@@ -47,7 +47,7 @@
<div class="row align-items-center">
<div class="col fs-5">
<span class="fw-bold"><TL>Shared IP</TL>:</span>
<span class="ms-1 text-muted">@($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation.Port}")</span>
<span class="ms-1 text-muted @(User.StreamerMode ? "blur" : "")">@($"{CurrentServer.Node.Fqdn}:{CurrentServer.MainAllocation.Port}")</span>
</div>
<div class="col fs-5">
<span class="fw-bold"><TL>Server ID</TL>:</span>

View File

@@ -1,212 +1,187 @@
@page "/servers"
@using Moonlight.App.Repositories.Servers
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Models.Misc
@using Moonlight.App.Repositories
@using Moonlight.App.Services
@using Newtonsoft.Json
@inject ServerRepository ServerRepository
@inject Repository<Server> ServerRepository
@inject Repository<User> UserRepository
@inject SmartTranslateService SmartTranslateService
@inject IServiceScopeFactory ServiceScopeFactory
@inject IJSRuntime JsRuntime
<LazyLoader Load="Load">
@if (AllServers.Any())
{
if (UseSortedServerView)
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="mx-auto">
<div class="card card-body">
<div class="d-flex justify-content-between">
<span class="badge badge-primary badge-lg px-5 me-4">Beta</span>
@if (EditMode)
{
var groupedServers = AllServers
.OrderBy(x => x.Name)
.GroupBy(x => x.Image.Name);
foreach (var groupedServer in groupedServers)
{
<div class="separator separator-content my-15">@(groupedServer.Key)</div>
<div class="card card-body bg-secondary py-0 my-0 mx-0 px-0">
@foreach (var server in groupedServer)
{
<div class="row mx-4 my-4">
<a class="card card-body" href="/server/@(server.Uuid)">
<div class="row">
<div class="col">
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-server"></i>
<div>
<WButton Text="@(SmartTranslateService.Translate("New group"))"
WorkingText=""
CssClasses="btn-primary me-3"
OnClick="AddGroup">
</WButton>
<WButton Text="@(SmartTranslateService.Translate("Finish editing layout"))"
CssClasses="btn-secondary"
OnClick="async () => await SetEditMode(false)">
</WButton>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/server/@(server.Uuid)" class="text-gray-800 text-hover-primary mb-1 fs-5">
@(server.Name)
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
@(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) <span class="text-gray-700">- @(server.Image.Name)</span>
</span>
</div>
</div>
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@if (StatusCache.ContainsKey(server))
{
var status = StatusCache[server];
switch (status)
{
case "offline":
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
case "stopping":
<span class="text-warning">
<TL>Stopping</TL>
</span>
break;
case "starting":
<span class="text-warning">
<TL>Starting</TL>
</span>
break;
case "running":
<span class="text-success">
<TL>Running</TL>
</span>
break;
case "failed":
<span class="text-gray-400">
<TL>Failed</TL>
</span>
break;
default:
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
}
}
else
{
<span class="text-gray-400">
<TL>Loading</TL>
</span>
<WButton Text="@(SmartTranslateService.Translate("Edit layout"))"
CssClasses="btn-secondary"
OnClick="async () => await SetEditMode(true)">
</WButton>
}
</div>
</div>
</a>
</div>
}
</div>
}
@foreach (var group in ServerGroups)
{
<div class="accordion my-3" id="serverListGroup@(group.GetHashCode())">
<div class="accordion-item">
<h2 class="accordion-header" id="serverListGroup-header@(group.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#serverListGroup-body@(group.GetHashCode())" aria-expanded="false" aria-controls="serverListGroup-body@(group.GetHashCode())">
<div class="d-flex justify-content-between">
<div>
@if (EditMode)
{
<input @bind="group.Name" class="form-control"/>
}
else
{
foreach (var server in AllServers)
if (string.IsNullOrEmpty(group.Name))
{
<div class="row px-5 mb-5">
<a class="card card-body" href="/server/@(server.Uuid)">
<div class="row">
<div class="col">
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-server"></i>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/server/@(server.Uuid)" class="text-gray-800 text-hover-primary mb-1 fs-5">
@(server.Name)
</a>
<span class="text-gray-400 fw-semibold d-block fs-6">
@(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) <span class="text-gray-700">- @(server.Image.Name)</span>
</span>
</div>
</div>
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@if (StatusCache.ContainsKey(server))
{
var status = StatusCache[server];
switch (status)
{
case "offline":
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
case "stopping":
<span class="text-warning">
<TL>Stopping</TL>
</span>
break;
case "starting":
<span class="text-warning">
<TL>Starting</TL>
</span>
break;
case "running":
<span class="text-success">
<TL>Running</TL>
</span>
break;
case "failed":
<span class="text-gray-400">
<TL>Failed</TL>
</span>
break;
default:
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
}
<TL>Unsorted servers</TL>
}
else
{
<span class="text-gray-400">
<TL>Loading</TL>
</span>
<span>@(group.Name)</span>
}
}
</div>
<div>
@if (EditMode)
{
<WButton Text="@(SmartTranslateService.Translate("Remove group"))"
WorkingText=""
CssClasses="btn-danger"
OnClick="async () => await RemoveGroup(group)">
</WButton>
}
</div>
</div>
</a>
</div>
}
}
}
else
{
<div class="alert bg-info d-flex flex-column flex-sm-row w-100 p-5">
<div class="d-flex flex-column pe-0 pe-sm-10">
<h4 class="fw-semibold">
<TL>You have no servers</TL>
</h4>
<span>
<TL>We were not able to find any servers associated with your account</TL>
</span>
</div>
</div>
}
</button>
</h2>
<div id="serverListGroup-body@(group.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="serverListGroup-header@(group.GetHashCode())" data-bs-parent="#serverListGroup">
<div class="accordion-body">
<div class="row min-h-200px draggable-zone" ml-server-group="@(group.Name)">
@foreach (var id in group.Servers)
{
var server = AllServers.FirstOrDefault(x => x.Id.ToString() == id);
<div class="row mt-7 px-3">
<div class="card">
if (server != null)
{
<div class="col-12 col-md-3 p-3 draggable" ml-server-id="@(server.Id)">
@if (EditMode)
{
<div class="card bg-secondary">
<div class="card-header">
<div class="card-title">
<span class="badge badge-primary badge-lg">Beta</span>
<span class="card-label">@(server.Name)</span>
</div>
<div class="card-toolbar">
<a href="#" class="btn btn-icon btn-sm btn-hover-light-primary draggable-handle">
<i class="bx bx-md bx-move"></i>
</a>
</div>
</div>
<div class="card-body">
<div class="row">
<label class="col-lg-4 col-form-label fw-semibold fs-6">Sorted server view</label>
<div class="col-lg-8 d-flex align-items-center">
<div class="form-check form-check-solid form-switch form-check-custom fv-row">
<input class="form-check-input w-45px h-30px" type="checkbox" id="sortedServerView" @bind="UseSortedServerView">
<label class="form-check-label" for="sortedServerView"></label>
<TL>Hidden in edit mode</TL>
</div>
</div>
}
else
{
<a class="invisible-a" href="/server/@(server.Uuid)">
<div class="card bg-secondary">
<div class="card-header">
<div class="card-title">
<span class="card-label">@(server.Name)</span>
</div>
</div>
<div class="card-body">
<span class="card-text fs-6">
@(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) <span class="text-gray-700">- @(server.Image.Name)</span>
</span>
<div class="card-text my-1 fs-6 fw-bold @(User.StreamerMode ? "blur" : "")">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="card-text fs-6">
@if (StatusCache.ContainsKey(server))
{
var status = StatusCache[server];
switch (status)
{
case "offline":
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
case "stopping":
<span class="text-warning">
<TL>Stopping</TL>
</span>
break;
case "starting":
<span class="text-warning">
<TL>Starting</TL>
</span>
break;
case "running":
<span class="text-success">
<TL>Running</TL>
</span>
break;
case "failed":
<span class="text-gray-400">
<TL>Failed</TL>
</span>
break;
default:
<span class="text-danger">
<TL>Offline</TL>
</span>
break;
}
}
else
{
<span class="text-gray-400">
<TL>Loading</TL>
</span>
}
</div>
</div>
</div>
</a>
}
</div>
}
}
</div>
</div>
</div>
</div>
</div>
</div>
}
</div>
</LazyLoader>
@code
@@ -215,11 +190,13 @@ else
public User User { get; set; }
private Server[] AllServers;
private LazyLoader LazyLoader;
private readonly Dictionary<Server, string> StatusCache = new();
private bool UseSortedServerView = false;
private List<ServerGroup> ServerGroups = new();
private bool EditMode = false;
private Task Load(LazyLoader arg)
private async Task Load(LazyLoader arg)
{
AllServers = ServerRepository
.Get()
@@ -231,6 +208,21 @@ else
.OrderBy(x => x.Name)
.ToArray();
if (string.IsNullOrEmpty(User.ServerListLayoutJson))
{
ServerGroups.Add(new()
{
Name = "",
Servers = AllServers.Select(x => x.Id.ToString()).ToList()
});
}
else
{
ServerGroups = (JsonConvert.DeserializeObject<ServerGroup[]>(
User.ServerListLayoutJson) ?? Array.Empty<ServerGroup>()).ToList();
}
foreach (var server in AllServers)
{
Task.Run(async () =>
@@ -248,6 +240,109 @@ else
}
});
}
}
private async Task AddGroup()
{
ServerGroups.Insert(0, new()
{
Name = "New group"
});
await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
}
private async Task RemoveGroup(ServerGroup group)
{
ServerGroups.Remove(group);
await EnsureAllServersInGroups();
await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
}
private async Task SetEditMode(bool toggle)
{
EditMode = toggle;
await InvokeAsync(StateHasChanged);
if (EditMode)
{
await EnsureAllServersInGroups();
await InvokeAsync(StateHasChanged);
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
}
else
{
var json = JsonConvert.SerializeObject(await GetGroupsFromClient());
User.ServerListLayoutJson = json;
UserRepository.Update(User);
await LazyLoader.Reload();
}
}
private async Task<ServerGroup[]> GetGroupsFromClient()
{
var serverGroups = await JsRuntime.InvokeAsync<ServerGroup[]>("moonlight.serverList.getData");
// Check user data to prevent users from doing stupid stuff
foreach (var serverGroup in serverGroups)
{
if (serverGroup.Name.Length > 30)
{
Logger.Verbose("Server list group lenght too long");
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;
}
private Task EnsureAllServersInGroups()
{
var presentInGroup = new List<Server>();
foreach (var group in ServerGroups)
{
foreach (var id in group.Servers)
presentInGroup.Add(AllServers.First(x => x.Id.ToString() == id));
}
var serversMissing = new List<Server>();
foreach (var server in AllServers)
{
if (presentInGroup.All(x => x.Id != server.Id))
serversMissing.Add(server);
}
if (serversMissing.Any())
{
var defaultGroup = ServerGroups.FirstOrDefault(x => x.Name == "");
if (defaultGroup == null)
{
defaultGroup = new ServerGroup()
{
Name = ""
};
ServerGroups.Add(defaultGroup);
}
foreach (var server in serversMissing)
defaultGroup.Servers.Add(server.Id.ToString());
}
return Task.CompletedTask;
}

View File

@@ -1,97 +0,0 @@
{
"Moonlight": {
"AppUrl": "http://your-moonlight-url.test",
"Database": {
"Database": "database_name",
"Host": "your-moonlight-database-host.de",
"Password": "s3cr3t",
"Port": "10324",
"Username": "user_name"
},
"DiscordBotApi": {
"Enable": false,
"Token": "you api key here"
},
"DiscordBot": {
"Enable": false,
"Token": "Discord.Token.Here",
"PowerActions": false
},
"Domains": {
"_comment": "Cloudflare Api Credentials",
"AccountId": "Account Id here",
"Email": "Cloudflare Email here",
"Key": "Api Key Here"
},
"Html": {
"Headers": {
"Color": "#4b27e8",
"Description": "the next generation hosting panel",
"Keywords": "moonlight",
"Title": "Moonlight - moonlight.tld"
}
},
"Marketing": {
"BrandName": "My cool project",
"Imprint": "https://mycoolproject.de/imprint",
"Privacy": "https://mycoolproject.de/privacy",
"Website": "https://mycoolproject.de/"
},
"OAuth2": {
"_exampleProviders": [
{
"Id": "discord",
"ClientId": "",
"ClientSecret": ""
},
{
"Id": "google",
"ClientId": "",
"ClientSecret": ""
}
],
"Providers": [],
"EnableOverrideUrl": false,
"OverrideUrl": "http://your-moonlight-url.test"
},
"Security": {
"Token": "RANDOM UUID HERE"
},
"Mail": {
"Email": "no-reply@mycoolproject.de",
"Server": "mycoolproject.de",
"Password": "s3cr3t",
"Port": 465,
"Ssl": true
},
"Cleanup": {
"Cpu": 90,
"Memory": 8192,
"Wait": 15,
"Uptime": 6,
"Enable": false,
"MinUptime": 10
},
"Subscriptions": {
"_comment": "Not implemented",
"SellPass": {
"Enable": false,
"Url": ""
}
},
"DiscordNotifications": {
"Enable": false,
"WebHook": ""
},
"Statistics": {
"Enabled": true,
"Wait": 15
},
"Rating": {
"Enabled": true,
"Url": "link-to-google.page",
"MinRating": 4,
"DaysSince": 5
}
}
}

View File

@@ -1,94 +1,94 @@
Open support;Support öffnen
Open support;Support Öffnen
About us;Über uns
Imprint;Impressum
Privacy;Datenschutz
Login;Anmelden
Privacy;Privacy
Login;Einloggen
Register;Registrieren
Insert brand name...;Markenname einfügen...
Save and continue;Speichern und fortfahren
Saving;Speichere
Configure basics;Grundlagen konfigurieren
Brand name;Markenname
test;Test
Insert first name...;Vornamen einfügen...
Insert last name...;Nachname einfügen...
Insert email address...;E-Mail Adresse einfügen...
Insert brand name...;Firmenname eingeben...
Save and continue;Speichern und Fortfahren
Saving;Speichern
Configure basics;Grundlagen einstellen
Brand name;Firmenname
test;test
Insert first name...;Vornamen eingeben...
Insert last name...;Nachnamen eingeben...
Insert email address...;E-Mail-Adresse eingeben...
Add;Hinzufügen
Adding...;Füge hinzu...
Add admin accounts;Admin-Konten hinzufügen
Adding...;Wird hinzugefügt...
Add admin accounts;Admin Konto hinzufügen
First name;Vorname
Last name;Nachname
Email address;Email-Adresse
Email address;E-Mail-Adresse
Enter password;Passwort eingeben
Next;Weiter
Back;Zurück
Configure features;Funktionen konfigurieren
Support chat;Support-Chat
Finish;Beenden
Finalize installation;Installation abschließen
Moonlight basic settings successfully configured;Moonlight Grundeinstellungen erfolgreich konfiguriert
Ooops. This page is crashed;Ooops. Diese Seite ist abgestürzt
This page is crashed. The error has been reported to the moonlight team. Meanwhile you can try reloading the page;Diese Seite ist abgestürzt. Der Fehler wurde an das Moonlight-Team gemeldet. In der Zwischenzeit kannst du versuchen, die Seite neu zu laden
Configure features;Features konfigurieren
Support chat;Chat Hilfe
Finish;Fertigstellen
Finalize installation;Installation Fertigstellen
Moonlight basic settings successfully configured;Moonlight's Standard-Einstellungen wurden konfiguriert
Ooops. This page is crashed;Ups. Die Seite ist abgestürzt.
This page is crashed. The error has been reported to the moonlight team. Meanwhile you can try reloading the page;Diese Seite ist abgestürzt. Bitte versuche, sie neu zu laden.
Setup complete;Einrichtung abgeschlossen
It looks like this moonlight instance is ready to go;Es sieht so aus, als ob diese Moonlight-Instanz einsatzbereit ist
It looks like this moonlight instance is ready to go;Diese Moonlight Instanz ist bereit
User successfully created;Benutzer erfolgreich erstellt
Ooops. Your moonlight client is crashed;Ups. Dein Moonlight-Client ist abgestürzt
This error has been reported to the moonlight team;Dieser Fehler wurde an das Moonlight-Team gemeldet
Ooops. Your moonlight client is crashed;Ups. Dein Moonlight ist abgestürzt.
This error has been reported to the moonlight team;Dieser Fehler wurde dem Moonlight-Team mitgeteilt
Sign In;Anmelden
Sign in to start with moonlight;Anmelden, um mit moonlight zu starten
Sign in with Discord;Mit Discord anmelden
Sign in to start with moonlight;Anmelden, um mit Moonlight zu starten
Sign in with Discord;Mit Discord Anmelden
Or with email;Oder mit E-Mail
Forgot password?;Passwort vergessen?
Forgot password?;Password vergessen?
Sign-in;Anmelden
Not registered yet?;Noch nicht registriert?
Sign up;Anmelden
Authenticating;Authentifiziere
Sign in with Google;Mit Google anmelden
Working;Läuft
Not registered yet?;Noch nicht Registriert?
Sign up;Registrieren
Authenticating;Authentifizieren...
Sign in with Google;Mit Google Anmelden
Working;Bitte warten...
Error;Fehler
Email and password combination not found;E-Mail- und Passwortkombination nicht gefunden
Email;E-Mail
Password;Passwort
Account settings;Kontoeinstellungen
Email and password combination not found;E-Mail und Password-Kombination wurden nicht gefunden
Email;E-mail
Password;Password
Account settings;Benutzer-Einstellungen
Logout;Abmelden
Dashboard;Dashboard
Order;Bestellung
Order;Bestellen
Website;Website
Database;Datenbank
Domain;Domain
Servers;Server
Websites;Webseiten
Websites;Websiten
Databases;Datenbanken
Domains;Domains
Changelog;Changelog
Changelog;Änderungen
Firstname;Vorname
Lastname;Nachname
Repeat password;Passwort wiederholen
Sign Up;Anmelden
Sign up to start with moonlight;Anmelden, um mit Moonlight zu beginnen
Sign up with Discord;Mit Discord anmelden
Sign up with Google;Mit Google anmelden
Sign-up;Anmelden
Already registered?;Bereits registriert?
Sign in;Anmelden
Sign up to start with moonlight;Registrieren um mit Moonlight zu starten
Sign up with Discord;Mit Discord Registrieren
Sign up with Google;Mit Google Registrieren
Sign-up;Registrieren
Already registered?;Schon Registriert?
Sign in;Registrieren
Create something new;Etwas Neues erstellen
Create a gameserver;Einen Gameserver erstellen
A new gameserver in just a few minutes;Ein neuer Gameserver in nur wenigen Minuten
Create a database;Erstelle eine Datenbank
A quick way to store your data and manage it from all around the world;Ein schneller Weg, um deine Daten zu speichern und sie von überall auf der Welt zu verwalten
Manage your services;Verwalte deine Services
Manage your gameservers;Verwalte deine Gameserver
A new gameserver in just a few minutes;Ein neuer Gameserver in wenigen Minuten
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, deine Daten von überall auf der Welt zu verwalten
Manage your services;Deine Dienste verwalten
Manage your gameservers;Gameserver verwalten
Adjust your gameservers;Deine Gameserver anpassen
Manage your databases;Verwalte deine Datenbanken
Insert, delete and update the data in your databases;Einfügen, Löschen und Aktualisieren der Daten in deinen Datenbanken
Manage your databases;Datenbanken verwalten
Insert, delete and update the data in your databases;Daten in die Datenbank einfügen, entfernen und ändern
Create a website;Eine Website erstellen
Make your own websites with a webspace;Eigene Websites mit einem Webspace erstellen
Make your own websites with a webspace;Mit einem Webspace eine Website erstellen
Create a domain;Eine Domain erstellen
Make your servvices accessible throught your own domain;Mache deine Services über deine eigene Domain zugänglich
Manage your websites;Verwalte deine Webseiten
Modify the content of your websites;Ändere den Inhalt deiner Websites
Manage your domains;Verwalte deine Domains
Add, edit and delete dns records;DNS-Einträge hinzufügen, bearbeiten und löschen
Make your servvices accessible throught your own domain;Mache deine Dienste mit einer Domain erreichbar
Manage your websites;Deine Websiten verwalten
Modify the content of your websites;Den Inhalt deiner Websiten verwalten
Manage your domains;Deine Domains verwalten
Add, edit and delete dns records;DNS-Records hinzufügen, entfernen oder bearbeiten
Admin;Admin
System;System
Overview;Übersicht
@@ -98,71 +98,71 @@ Nodes;Nodes
Images;Images
aaPanel;aaPanel
Users;Benutzer
Support;Support
Support;Hilfe
Statistics;Statistiken
No nodes found. Start with adding a new node;Keine Nodes gefunden. Beginne mit dem Hinzufügen einer neuen Node
No nodes found. Start with adding a new node;Keine Nodes gefunden. Eine neue Node hinzufügen
Nodename;Nodename
FQDN;FQDN
Create;Erstellen
Creating;Erstelle
Http port;Http-Port
Sftp port;Sftp-Port
Creating;Wird erstellt...
Http port;Http Port
Sftp port;Sftp Port
Moonlight daemon port;Moonlight Daemon Port
SSL;SSL
CPU Usage;CPU-Auslastung
CPU Usage;CPU Auslastung
In %;In %
Memory;Speicher
Used / Available memory;Verwendeter / Verfügbarer Speicher
Storage;Speicher
Available storage;Verfügbarer Speicher
Memory;Arbeitsspeicher
Used / Available memory;Benutzter / Verfügbarer Arbeitsspeicher
Storage;Speicherplatz
Available storage;Verfügbarer Speicherplatz
Add a new node;Eine neue Node hinzufügen
Delete;Löschen
Deleting;Lösche
Deleting;Wird gelöscht...
Edit;Bearbeiten
Token Id;Token Id
Token Id;Token ID
Token;Token
Save;Speichern
Setup;Einrichten
Open a ssh connection to your node and enter;Öffne eine SSH-Verbindung zu deiner Node und gebe
and paste the config below. Then press STRG+O and STRG+X to save;und füge die unten stehende Konfiguration ein. Drücke dann STRG+O und STRG+X, um zu speichern
Before configuring this node, install the daemon;Bevor du diese Node konfigurierst, installiere den Daemon
Setup;Aufsetzen
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ü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
Delete this node?;Diese Node löschen?
Do you really want to delete this node;Willst du diese Node wirklich löschen
Do you really want to delete this node;Möchtest du diese Node wirklich löschen?
Yes;Ja
No;Nein
Status;Status
Adding;Füge hinzu
Adding;Hinzufügen
Port;Port
Id;Id
Id;ID
Manage;Verwalten
Create new server;Neuen Server erstellen
No servers found;Keine Server gefunden
Server name;Servername
Server name;Server Name
Cpu cores;CPU Kerne
Disk;Datenträger
Disk;Speicherplatz
Image;Image
Override startup;Startup überschreiben
Docker image;Docker-Image
CPU Cores (100% = 1 Core);CPU-Kerne (100% = 1 Kern)
Docker image;Docker Image
CPU Cores (100% = 1 Core);CPU Kerne (100% = 1 Kern)
Server successfully created;Server erfolgreich erstellt
Name;Name
Cores;CPU Kerne
Owner;Eigentümer
Cores;Kerne
Owner;Besitzer
Value;Wert
An unknown error occured;Ein unbekannter Fehler ist aufgetreten
No allocation found;Keine Allocation gefunden
Identifier;Identifier
UuidIdentifier;UUIDIdentifier
Override startup command;Startbefehl überschreiben
Loading;Lade
Override startup command;Startup Befehl überschreiben
Loading;Wird geladen...
Offline;Offline
Connecting;Verbinde
Start;Starten
Connecting;Verbiden...
Start;Start
Restart;Neustarten
Stop;Stoppen
Shared IP;Shared IP
Server ID;Server-ID
Cpu;Cpu
Shared IP;Geteilte IP
Server ID;Server ID
Cpu;CPU
Console;Konsole
Files;Dateien
Backups;Backups
@@ -171,397 +171,466 @@ Plugins;Plugins
Settings;Einstellungen
Enter command;Befehl eingeben
Execute;Ausführen
Checking disk space;Prüfe Speicherplatz
Updating config files;Aktualisiere Konfigurationsdateien
Checking file permissions;Prüfe Dateiberechtigungen
Downloading server image;Herunterladen des Serverabbilds
Downloaded server image;Heruntergeladenes Serverabbild
Starting;Starte
Checking disk space;Speicherplatz überprüfen
Updating config files;Konfigurations-Dateien werden geupdatet
Checking file permissions;Datei-Rechte werden überprüft
Downloading server image;Server Image wird heruntergeladen
Downloaded server image;Server Image wurde heruntergeladen
Starting;Startet
Online;Online
Kill;Killen
Stopping;Stoppe
Search files and folders;Dateien und Verzeichnisse durchsuchen
Kill;Kill
Stopping;Stoppt
Search files and folders;Ordner und Dateien durchsuchen
Launch WinSCP;WinSCP starten
New folder;Neuer Ordner
Upload;Hochladen
File name;Dateiname
File size;Dateigröße
Last modified;Letzte Änderung
Last modified;Zuletzt geändert
Cancel;Abbrechen
Canceling;Breche ab
Running;Laufend
Loading backups;Lade Backups
Started backup creation;Backup-Erstellung gestartet
Backup is going to be created;Backup wird erstellt
Canceling;Wird Abgebrochen
Running;Läuft
Loading backups;Backups werden geladen
Started backup creation;Backup wird erstellt
Backup is going to be created;Backup wird erstellt werden
Rename;Umbenennen
Move;Verschieben
Move;Bewegen
Archive;Archivieren
Unarchive;Unarchivieren
Unarchive;Archivieren rückgängig machen
Download;Herunterladen
Starting download;Starte Download
Backup successfully created;Backup erfolgreich erstellt
Starting download;Download wird gestartet
Backup successfully created;Backup wurde erfolgreich erstellt
Restore;Wiederherstellen
Copy url;Url kopieren
Backup deletion started;Backup-Löschung gestartet
Backup successfully deleted;Backup erfolgreich gelöscht
Primary;Primär
This feature is currently not available;Diese Funktion ist derzeit nicht verfügbar
Copy url;URL Kopieren
Backup deletion started;Backup Löschung wird gestartet
Backup successfully deleted;Backup wurde erfolgreich gelöscht
Primary;Primärer
This feature is currently not available;Diese Funktion ist zurzeit leider nicht verfügbar
Send;Senden
Sending;Sende
Welcome to the support chat. Ask your question here and we will help you;Willkommen im Support-Chat. Stelle hier deine Frage und wir werden dir helfen
minutes ago; vor Minuten
just now;jetzt
Sending;Wird gesendet
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 her
just now;gerade eben
less than a minute ago;vor weniger als einer Minute
1 hour ago;vor 1 Stunde
1 minute ago;vor 1 Minute
1 hour ago;vor einer Stunde
1 minute ago;vor einer Minute
Failed;Fehlgeschlagen
hours ago; vor Stunden
Open tickets;Offene Tickets
hours ago; Stunden her
Open tickets;Tickets öffnen
Actions;Aktionen
No support ticket is currently open;Kein Supportticket ist zurzeit offen
User information;Benutzerinformationen
Close ticket;Anfrage schließen
Closing;Schließe
The support team has been notified. Please be patient;Das Support-Team wurde benachrichtigt. Bitte habe Geduld
The ticket is now closed. Type a message to open it again;Das Ticket ist jetzt geschlossen. Gib eine Nachricht ein, um es wieder zu öffnen
1 day ago;Vor 1 Tag
is typing;tippt gerade
are typing;tippen gerade
No support ticket is currently open;Kein Support Ticket ist zurzeit offen.
User information;Benutzer-Information
Close ticket;Ticket schließen
Closing;Wird geschlossen...
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
1 day ago;vor einem Tag
is typing;schreibt...
are typing;schreiben...
No domains available;Keine Domains verfügbar
Shared domains;Gemeinsame Domains
Shared domain;Geteilte Domains
Shared domain successfully deleted;Geteilte Domain erfolgreich gelöscht
Shared domain successfully added;Geteilte Domain erfolgreich hinzugefügt
Domain name;Domainname
DNS records for;DNS-Einträge für
Fetching dns records;Rufe DNS-Einträge ab
No dns records found;Keine DNS-Einträge gefunden
Shared domains;Geteilte Domains
Shared domain;Geteilte Domain
Shared domain successfully deleted;Geteilte Domain wurde erfolgreich gelöscht
Shared domain successfully added;Geteilte Domain wurde erfolgreich hinzugefügt
Domain name;Domain Name
DNS records for;DNS-Record für
Fetching dns records;Es wird nach DNS-Records gesucht
No dns records found;Keine DNS-Records gefunden
Content;Inhalt
Priority;Priorität
Ttl;TTL
Enable cloudflare proxy;Cloudflare Proxy einschalten
Enable cloudflare proxy;Cloudflare-Proxy benutzen
CF Proxy;CF Proxy
days ago; Tage vergangen
days ago; Tage her
Cancle;Abbrechen
An unexpected error occured;Ein unerwarteter Fehler ist aufgetreten
An unexpected error occured;Ein unbekannter Fehler ist aufgetreten
Testy;Testy
Error from cloudflare api;Fehler von der Cloudflare API
Error from cloudflare api;Fehler von der Cloudflare-API
Profile;Profil
No subscription available;Kein Abonnement verfügbar
Buy;Kaufen
Redirecting;Leite um
Redirecting;Weiterleiten
Apply;Anwenden
Applying code;Code anwenden
Invalid subscription code;Ungültiger Abo-Code
Cancel Subscription;Abonnement kündigen
Applying code;Code Anwenden
Invalid subscription code;Unbekannter Abo-Code
Cancel Subscription;Abo beenden
Active until;Aktiv bis
We will send you a notification upon subscription expiration;Wir senden dir eine Benachrichtigung, wenn dein Abonnement abläuft
This token has been already used;Dieser Token wurde bereits verwendet
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
New login for;Neue Anmeldung für
No records found for this day;Keine Daten für diesen Tag gefunden
No records found for this day;Für diesen Tag wurden keine Records gefunden
Change;Ändern
Changing;Ändere
Minecraft version;Minecraft-Version
Build version;Build-Version
Server installation is currently running;Serverinstallation läuft derzeit
Changing;Wird geändert
Minecraft version;Minecraft Version
Build version;Build Version
Server installation is currently running;Der Server wird installiert.
Selected;Ausgewählt
Move deleted;Verschiebe das Gelöschte
Delete selected;Lösche das Ausgewählte
Log level;Log-Stufe
Log message;Logmeldung
Move deleted;Gelöschtes Bewegen
Delete selected;Ausgewähltes löschen
Log level;Log Level
Log message;Log Message
Time;Zeit
Version;Version
You are running moonlight version;Du verwendest die Moonlight-Version
You are running moonlight version;Du benutzt die Moonlight-Version
Operating system;Betriebssystem
Moonlight is running on;Moonlight läuft auf
Memory usage;Speichernutzung
Moonlight is using;Moonlight verwendet
of memory;Speicherplatz
Cpu usage;CPU Verbrauch
Refresh;Aktualisieren
Memory usage;Arbeitsspeicher Auslastung
Moonlight is using;Moonlight benutzt
of memory;des Arbeitsspeichers
Cpu usage;CPU Auslastung
Refresh;Neuladen
Send a message to all users;Eine Nachricht an alle Benutzer senden
IP;IP
URL;URL
Device;Gerät
Change url;URL ändern
Change url;URL Ändern
Message;Nachricht
Enter message;Nachricht eingeben
Enter the message to send;Zu sendende Nachricht eingeben
Enter the message to send;Eine Nachricht zum Senden eingeben
Confirm;Bestätigen
Are you sure?;Bist du dir sicher?
Enter url;URL eingeben
An unknown error occured while starting backup deletion;Ein unbekannter Fehler ist beim Starten des Löschvorgangs des Backups aufgetreten
Success;Erfolg
Backup URL successfully copied to your clipboard;Backup-URL erfolgreich in die Zwischenablage kopiert
Backup restore started;Wiederherstellung des Backups gestartet
Backup successfully restored;Backup erfolgreich wiederhergestellt
An unknown error occured while starting backup deletion;Ein unbekannter Fehler ist während der Backuplöschung aufgetreten
Success;erfolgreich
Backup URL successfully copied to your clipboard;Die Backup URL wurde in deine Zwischenablage kopiert
Backup restore started;Backup wiederherstellung gestartet
Backup successfully restored;Das Backup wurde erfolgreich wiedergeherstellt
Register for;Registrieren für
Core;Kern
Logs;Logs
AuditLog;AuditLog
SecurityLog;Sicherheitslog
ErrorLog;Fehlerlog
Resources;Ressourcen
WinSCP cannot be launched here;WinSCP kann hier nicht gestartet werden
Create a new folder;Einen neuen Ordner erstellen
Enter a name;Namen eingeben
File upload complete;Datei-Upload abgeschlossen
AuditLog;Audit Log
SecurityLog;Security Log
ErrorLog;Error Log
Resources;Resourcen
WinSCP cannot be launched here;WinSCP kann nicht gestartet werden
Create a new folder;Neuen Ordner erstellen
Enter a name;Einen Namen eingeben
File upload complete;Dateiupload abgeschlossen
New server;Neuer Server
Sessions;Sitzungen
New user;Neuer Benutzer
Created at;Erstellt am
Mail template not found;Mailvorlage nicht gefunden
Missing admin permissions. This attempt has been logged ;Fehlende Admin-Berechtigungen. Dieser Versuch wurde geloggt
Address;Adresse
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 und ist für das ganze Admin Team sichtbar
Address;Addresse
City;Stadt
State;Bundesland
Country;Land
Totp;Totp
State;Land
Country;Staat
Totp;TOTP
Discord;Discord
Subscription;Abonnement
None;Keine
None;None
No user with this id found;Kein Benutzer mit dieser ID gefunden
Back to list;Zurück zur Liste
New domain;Neue Domain
Reset password;Passwort zurücksetzen
Password reset;Passwort zurücksetzen
Reset the password of your account;Passwort für dein Konto zurücksetzen
Reset password;Password wiederherstellen
Password reset;Password wiederherstellung
Reset the password of your account;Password deines Accounts zurücksetzen
Wrong here?;Falsch hier?
A user with this email can not be found;Ein Benutzer mit dieser E-Mail Adresse kann nicht gefunden werden
Passwort reset successfull. Check your mail;Passwort zurücksetzen erfolgreich. Überprüfe deine Mails
Discord bot;Discord-Bot
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 dein Email Postfach
Discord bot;Discord Bot
New image;Neues Image
Description;Beschreibung
Uuid;UUID
Enter tag name;Tag-Name eingeben
Enter tag name;Tag Namen eingeben
Remove;Entfernen
No tags found;Keine Tags gefunden
Enter docker image name;Name des Docker-Images eingeben
Enter docker image name;Docker Image Namen eingeben
Tags;Tags
Docker images;Docker-Images
Default image;Standardimage
Startup command;Startup-Befehl
Install container;Container installieren
Install entry;Eintrag installieren
Docker images;Docker Images
Default image;Standard Image
Startup command;Startup Befehl
Install container;Install container
Install entry;Install entry
Configuration files;Konfigurationsdateien
Startup detection;Startup-Erkennung
Startup detection;Startuperkennung
Stop command;Stopp-Befehl
Successfully saved image;Image erfolgreich gespeichert
Successfully saved image;Das Image wurde erfolgreich gespeichert
No docker images found;Keine Docker Images gefunden
Key;Schlüssel
Key;Key
Default value;Standardwert
Allocations;Allocations
Allocations;Zuweisung
No variables found;Keine Variablen gefunden
Successfully added image;Image erfolgreich hinzugefügt
Password change for;Passwortänderung für
Successfully added image;Das Image wurde erfolgreich hinzugefügt
Password change for;Password ändern für
of;von
New node;Neue Node
Fqdn;FQDN
Cores used;Verwendete CPU Kerne
used;verwendet
Cores used;Kerne genutzt
used;benutzt
5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
Host system information;Host-System-Informationen
Host system information;Host System Information
0;0
Docker containers running;Laufende Docker-Container
Docker containers running;Laufende Docker Container
details;Details
1;1
2;2
DDos;DDos
No ddos attacks found;Keine DDos-Angriffe gefunden
No ddos attacks found;Keine DDos Attacken gefunden
Node;Node
Date;Datum
DDos attack started;DDos-Angriff gestartet
DDos attack started;DDos Attacke gestartet
packets;Pakete
DDos attack stopped;DDos-Angriff gestoppt
DDos attack stopped;DDos Attacke gestoppt
packets; Pakete
Stop all;Alle stoppen
Kill all;Alle killen
Network in;Netzwerk ein
Network out;Netzwerk raus
Kill all servers;Alle Server killen
Do you really want to kill all running servers?;Willst du wirklich alle laufenden Server killen?
Change power state for;Power State ändern für
Stop all;Alle Stoppen
Kill all;Allen Killen
Network in;Network in
Network out;Network out
Kill all servers;Alle Server Killen
Do you really want to kill all running servers?;Möchtest du wirklich alle laufenden Server Killen?
Change power state for;Power-State ändern für
to;zu
Stop all servers;Alle Server stoppen
Do you really want to stop all running servers?;Willst du wirklich alle laufenden Server stoppen?
Do you really want to stop all running servers?;Möchtest du wirklich alle laufenden Server stoppen?
Manage ;Verwalten
Manage user ;Benutzer verwalten
Reloading;Lade neu
Reloading;Lade neu...
Update;Aktualisieren
Updating;Aktualisiere
Updating;Wird Aktualisiert...
Successfully updated user;Benutzer erfolgreich aktualisiert
Discord id;Discord ID
Discord username;Discord-Benutzername
Discord discriminator;Discord-Diskriminator
The Name field is required.;Das Feld Name ist erforderlich
An error occured while logging you in;Beim Einloggen ist ein Fehler aufgetreten
You need to enter an email address;Du musst eine E-Mail Adresse eingeben
You need to enter a password;Du musst ein Passwort eingeben
You need to enter a password with minimum 8 characters in lenght;Du musst ein Passwort mit einer länge von mindestens 8 Zeichen eingeben
Proccessing;Bearbeite
The FirstName field is required.;Das Feld Vorname ist erforderlich.
The LastName field is required.;Das Feld Nachname ist erforderlich.
The Address field is required.;Das Adressfeld ist erforderlich.
The City field is required.;Das Feld Stadt ist erforderlich.
The State field is required.;Das Feld Bundesland ist erforderlich.
The Country field is required.;Das Feld Land ist erforderlich.
Discord id;Discord User ID
Discord username;Discord Benutzername
Discord discriminator;Discord Tag
The Name field is required.;Der Name dieses Feldes ist erforderlich
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 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
Proccessing;Weid verarbeitet...
The FirstName field is required.;Das Vorname-Feld ist erforderlich
The LastName field is required.;Das Nachname-Feld ist erforderlich
The Address field is required.;Das Addresse-Feld ist erforderlich
The City field is required.;Das Stadt-Feld ist erforderlich
The State field is required.;Das Staat-Feld ist erforderlich
The Country field is required.;Das Land-Feld ist erforderlich
Street and house number requered;Straße und Hausnummer erforderlich
Max lenght reached;Maximale Länge erreicht
Server;Server
stopped;gestoppt
Cleanups;Cleanups
executed;ausgeführt
Used clanup;Benutztes Cleanup
Used clanup;Cleanup benutzt
Enable;Aktivieren
Disabble;Deaktivieren
Disable;Deaktivieren
Addons;Addons
Addons;Add-ons
Javascript version;Javascript Version
Javascript file;Javascript-Datei
Select javascript file to execute on start;Javascript-Datei zum Ausführen beim Start auswählen
Submit;Absenden
Processing;Verarbeite
Go up;Nach oben
Running cleanup;Laufende Cleanup Server
servers;Server
Select folder to move the file(s) to;Ordner auswählen, in den die Datei(en) verschoben werden sollen
Paper version;Paperversion
Javascript file;Javascript Datei
Select javascript file to execute on start;Javascript Datei zum Starten auswählen
Submit;Einreichen
Processing;Wird verarbeitet...
Go up;Nach oben gehen
Running cleanup;Cleanup läuft
servers;Servers
Select folder to move the file(s) to;Ordner zum Bewegen der Dateien auswählen
Paper version;Paper Version
Join2Start;Join2Start
Server reset;Server zurücksetzen
Reset;Zurücksetzen
Resetting;Setze zurück
Are you sure you want to reset this server?;Bist du dir sicher, dass du diesen Server zurücksetzen willst?
Resetting;Wird zurückgesetzt
Are you sure you want to reset this server?;Möchtest du diesen Server wirklich zurücksetzen?
Are you sure? This cannot be undone;Bist du dir sicher? Dies kann nicht rückgängig gemacht werden
Resetting server;Setze Server zurück
Deleted file;Gelöschte Datei
Reinstalling server;Installiere den Server neu
Uploading files;Lade Dateien hoch
Resetting server;Server wird zurückgesetzt...
Deleted file;Datei gelöscht
Reinstalling server;Server wird neuinstalliert
Uploading files;Dateien wurden hochgeladen
complete;vollständig
Upload complete;Upload vollständig
Upload complete;Upload komplett
Security;Sicherheit
Subscriptions;Abonnements
2fa Code;2FA Code
Your account is secured with 2fa;Dein Konto ist mit 2FA gesichert
anyone write a fancy text here?;Kann hier jemand einen netten Text schreiben? Ja sicher: Ich war hier. - Dannyx
Activate 2fa;2FA aktivieren
2fa apps;2FA Apps
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? -Nö.
Activate 2fa;2-FA Aktivieren
2fa apps;2-FA Apps
Use an app like ;Benutze eine App wie
or;oder
and scan the following QR Code;und scanne den folgenden QR-Code
If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:;Wenn du Probleme bei der Verwendung des QR-Codes hast, wähle in der App manuelle Eingabe und gib deine E-Mail-Adresse und den folgenden Code ein:
Finish activation;Aktivierung beenden
2fa Code requiered;2FA-Code erforderlich
New password;Neues Passwort
Secure your account;Sichere dein Konto
2fa adds another layer of security to your account. You have to enter a 6 digit code in order to login.;2FA fügt eine weitere Sicherheitsebene zu deinem Konto hinzu. Du musst dann noch einen 6-stelligen Code eingeben, um dich anzumelden.
and scan the following QR Code;und scanne diesen QR-Code
If you have trouble using the QR Code, select manual input in the app and enter your email and the following code:;Wenn du Probleme mit dem Scannen des Qr-Codes has, benutze doch die Manuelle Eingabe der App und gib deine E-Mail und den folgenden Code ein:
Finish activation;Aktivierung fertig
2fa Code requiered;2-FA Code erforderlich
New password;Neues Password
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.
New subscription;Neues Abonnement
You need to enter a name;Du musst einen Namen eingeben
You need to enter a description;Du musst eine Beschreibung eingeben
Add new limit;Neues Limit hinzufügen
You need to enter a description;Du musst eine Beschreibung eigeben
Add new limit;Ein neues Limit hinzufügen
Create subscription;Abonnement erstellen
Options;Optionen
Amount;Betrag
Do you really want to delete it?;Willst du es wirklich löschen?
Loading your subscription;Abonnement laden
Searching for deploy node;Suche nach einer Deploy Node
Searching for available images;Suche nach verfügbaren Images
Server details;Server-Details
Configure your server;Konfiguriere deinen Server
Default;Standard
You reached the maximum amount of servers for every image of your subscription;Du hast die maximale Anzahl von Servern für jedes Image deines Abonnements erreicht
Do you really want to delete it?;Möchtes du es wirklich löschen?
Loading your subscription;Dein Abonnement wird geladen
Searching for deploy node;Suche nach einer verfügbaren Node
Searching for available images;Nach verfügbaren Images wird gesucht
Server details;Server Details
Configure your server;Deinen Server konfigurieren
Default;Standart
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;Persönliche Informationen
Enter code;Code eingeben
Server rename;Server umbenennen
Server rename;Server Umbenennen
Create code;Code erstellen
Save subscription;Abonnement speichern
Enter your information;Gib deine Informationen ein
You need to enter your full name in order to use moonlight;Du musst deinen vollständigen Namen eingeben, um Moonlight zu verwenden
No node found;Keine Node gefunden
No node found to deploy to found;Kein Node zum Deployen gefunden
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
No node found;Kein Node gefunden
No node found to deploy to found;Keine Node für die Bereitstellung gefunden
Node offline;Node offline
The node the server is running on is currently offline;Die Node, auf der der Server läuft, ist derzeit offline
Server not found;Server nicht gefunden
A server with that id cannot be found or you have no access for this server;Ein Server mit dieser ID kann nicht gefunden werden oder du hast keinen Zugriff auf diesen Server
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
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
Decompress;Dekomprimieren
Moving;Verschieben
Compressing;Komprimieren
selected;ausgewählt
Decompress;De-Komprimieren
Moving;Bewegen...
Compressing;Komprimieren...
selected;Ausgewählt
New website;Neue Website
Plesk servers;Plesk-Server
Base domain;Basisdomain
Plesk server;Plesk-Server
Plesk servers;Plesk Servers
Base domain;Base Domain
Plesk server;Plesk Server
Ftp;FTP
No SSL certificate found;Kein SSL-Zertifikat gefunden
No SSL certificate found;Keine SSL-Zertifikate gefunden
Ftp Host;FTP Host
Ftp Port;FTP Port
Ftp Username;FTP Benutzername
Ftp Username;FTP Username
Ftp Password;FTP Passwort
Use;Verwenden
SSL Certificates;SSL-Zertifikate
SSL certificates;SSL-Zertifikate
Issue certificate;Zertifikat ausstellen lassen
New plesk server;Neuer Plesk-Server
Api url;API-URL
Host system offline;Hostsystem offline
The host system the website is running on is currently offline;Das Hostsystem, auf dem die Website läuft, ist derzeit offline
Use;Benutzen
SSL Certificates;SSL Zertifikate
SSL certificates;SSL Zertifikate
Issue certificate;SSL-Zertifikat erstellen lassen
New plesk server;Neuer Plesk Server
Api url;API URL
Host system offline;Host System Offline
The host system the website is running on is currently offline;Das Host System, auf dem diese Website läuft, ist offline
No SSL certificates found;Keine SSL-Zertifikate gefunden
No databases found for this website;Keine Datenbanken für diese Website gefunden
The name should be at least 8 characters long;Der Name muss mindestens 8 Zeichen lang sein
The name should only contain of lower case characters and numbers;Der Name darf nur aus Kleinbuchstaben und Zahlen bestehen
Error from plesk;Fehler von Plesk
No databases found for this website;Dieser Website konnten keine Datenbanken zugeordnet werden
The name should be at least 8 characters long;Der Name sollte mindestens 8 Zeichen lang sein
The name should only contain of lower case characters and numbers;Der Name sollte nur Kleinbuchstaben und Zahlen enthalten
Error from plesk;Error von Plesk
Host;Host
Username;Benutzername
SRV records cannot be updated thanks to the cloudflare api client. Please delete the record and create a new one;SRV-Einträge können aufgrund des Cloudflare-Api-Clients nicht aktualisiert werden. Bitte lösche den Eintrag und erstelle einen neuen
The User field is required.;Das Feld Benutzer ist erforderlich
You need to specify a owner;Du musst einen Besitzer angeben
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
You need to specify a owner;Du musst einen Server-Besitzer angeben
You need to specify a image;Du musst ein Image angeben
Api Url;API URL
Api Key;API Key
Api Key;Api Key
Duration;Dauer
Enter duration of subscription;Dauer des Abonnements eingeben
Copied code to clipboard;Code in die Zwischenablage kopiert
Invalid or expired subscription code;Ungültiger oder abgelaufener Abo-Code
Current subscription;Aktuelles Abonnement
You need to specify a server image;Du musst ein Server-Image angeben
Invalid or expired subscription code;Ungültiger oder Abgelaufener Abo-Code
Current subscription;Dein Abonnement
You need to specify a server image;Du musst ein Image angeben
CPU;CPU
Hour;Stunde
Day;Tag
Month;Monat
Year;Jahr
All time;Seit Beginn
This function is not implemented;Diese Funktion ist nicht eingebaut
Domain details;Domaindetails
Configure your domain;Konfiguriere deine Domain
You reached the maximum amount of domains in your subscription;Du hast die maximale Anzahl an Domains in deinem Abonnement erreicht
You need to specify a shared domain;Du musst eine gemeinsame Domain angeben
A domain with this name does already exist for this shared domain;Eine Domain mit diesem Namen existiert bereits für diese gemeinsame Domain
The Email field is required.;Das Feld E-Mail ist erforderlich.
The Password field is required.;Das Feld Passwort ist erforderlich.
The ConfirmPassword field is required.;Das Feld Passwort bestätigen ist erforderlich.
Passwords need to match;Passwörter müssen übereinstimmen
All time;Für immer
This function is not implemented;Diese Funktion wurde noch nicht hinzugefügt
Domain details;Domain Details
Configure your domain;Deine Domain konfigurieren
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
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 Password field is required.;Das Password-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
Cleanup exception;Cleanup Ausnahme
No shared domain found;Keine gemeinsame Domain gefunden
Searching for deploy plesk server;Suche nach einem Deploy Plesk Server
No shared domain found;Keine Shared-Domain gefunden
Searching for deploy plesk server;Suchen um den Plesk Server aufzusetzen
No plesk server found;Kein Plesk Server gefunden
No plesk server found to deploy to;Kein Plesk Server für die Bereitstellung gefunden
No node found to deploy to;Keine Node für die Bereitstellung gefunden
Website details;Webseiten-Details
No plesk server found to deploy to;Keinen Plesk Server zum Aufsetzen gefunden
No node found to deploy to;Kein Node zum Aufsetzen
Website details;Website Details
Configure your website;Konfiguriere deine Website
The name cannot be longer that 32 characters;Der Name darf nicht länger als 32 Zeichen sein
The name should only consist of lower case characters;Der Name darf nur aus Kleinbuchstaben bestehen
The name cannot be longer that 32 characters;Der Name kann nicht länger als 32 Zeichen sein
The name should only consist of lower case characters;Der Name sollte nur aus Kleinbuchstaben bestehen
News;Neuigkeiten
Title...;Titel...
Enter text...;Text eingeben...
Saving...;Speichere...
Deleting...;Lösche...
Delete post;Beitrag löschen
Do you really want to delete the post ";Willst du den Beitrag wirklich löschen "
Enter text...;Text einfügen...
Saving...;Wird gespeichert...
Deleting...;Wird gelöscht...
Delete post;Post löschen
Do you really want to delete the post ";Post löschen? "
You have no domains;Du hast keine Domains
We were not able to find any domains associated with your account;Wir konnten keine Domains finden, die mit deinem Konto verbunden sind
We were not able to find any domains associated with your account;Wir haben keine Domains, die mit deinem Account verbunden sind, gefunden
You have no websites;Du hast keine Websites
We were not able to find any websites associated with your account;Wir konnten keine Webseiten finden, die mit deinem Konto verbunden sind
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
You need a domain;Du brauchst eine Domain
New post;Neuer Post
New entry;Neuer Eintrag
You have no servers;Du hast keine Server
We were not able to find any servers associated with your account;Wir haben keine Server, die mit deinem Account verbunden sind, gefunden
Error creating server on wings;Fehler bei der Erstellung des Servers auf Wings
An unknown error occured while restoring a backup;Ein unbekannter Fehler ist während der Backup-Wiederherstellung aufgetreten
Error from daemon;Fehler vom Daemon
End;Ende
Cloud panel;Cloud Panel
Cloud panels;Cloud Panels
New cloud panel;Neues Cloud Panel
You need to enter an api key;Du musst einen API-Key eigeben
Webspaces;Webspaces
New webspace;Neuer Webspace
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 Upload aufgetreten
No databases found for this webspace;Keine Datenbanken für diesen Webspace gefunden
Sftp;SFTP
Sftp Host;SFTP Host
Sftp Port;SFTP Port
Sftp Username;SFTP Benutzername
Sftp Password;SFTP Password
Lets Encrypt certificate successfully issued;Lets Encrypt Zertifikat erfolgreich erstellt
Add shared domain;Shared Domain Hinzufügen
Webspace;Webspace
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
Webspace details;Webspace Details
Web host;Web host
Configure your webspaces;Konfiguriere deine Webspaces
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
Manage your webspaces;Deine Webspaces verwalten
Modify the content of your webspaces;Den Inhalt deiner Webspaces verwalten
Successfully updated password;Password erfolgreich geupdatet
An unknown error occured while sending your message;Ein unbekannter Fehler ist während dem Senden von deiner Nachricht aufgetreten
Open chats;Offene Chats
No message sent yet;Keine Nachrichten gesendet
Support ticket open;Support-Ticket geöffnet
Support ticket closed;Support-Ticket geschlossen
Your connection has been paused;Deine Verbindung wurde pausiert
We paused your connection because of inactivity. The resume just focus the tab and wait a few seconds;Wir haben deine Verbindung aufgrund von inaktivität pausiert. Wechsle auf den Tab und warte ein paar Sekunden
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
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 aktualisiere die Seite
Verifying token, loading user data;Token verifizieren, lade Benutzerdaten
Reload config;Konfiguration neuladen
Successfully reloading configuration;Konfiguration wird neugeladen...
Successfully reloaded configuration;Die Konfiguration wurde erfolgreich neugeladen
Flows;Flows
Add node;Node Hinzufügen
Web system;Web System
Servers with this image;Server mit diesem Image
You need to specify a user;Du musst einen Benutzer angeben
Import;Importieren
Export;Exportieren
Exporting;Wird exportiert
Successfully imported image;Das Image wurde erfolgreich importiert.
Forge version;Forge Version
Fabric version;Fabric Version
Fabric loader version;Fabric Loader Version
Rate;Rate
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:
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
Close;Schließen
Rating saved;Bewertung gespeichert
Group;Gruppe
Beta;Beta
Create a new group;Eine neue Gruppe erstellen

View File

@@ -16,6 +16,10 @@
filter: none;
}
.blur {
filter: blur(5px);
}
div.wave {
}
div.wave .dot {

View File

@@ -355,8 +355,6 @@
{
// filter here what key events should be sent to moonlight
console.log(event);
if(event.code === "KeyS" && event.ctrlKey)
{
event.preventDefault();
@@ -370,5 +368,54 @@
{
window.removeEventListener('keydown', moonlight.keyListener.listener);
}
},
serverList: {
init: function ()
{
if(moonlight.serverList.Swappable)
{
moonlight.serverList.Swappable.destroy();
}
let containers = document.querySelectorAll(".draggable-zone");
if (containers.length !== 0)
{
moonlight.serverList.Swappable = new Draggable.Sortable(containers, {
draggable: ".draggable",
handle: ".draggable .draggable-handle",
mirror: {
//appendTo: selector,
appendTo: "body",
constrainDimensions: true
}
});
}
},
getData: function ()
{
let groups = new Array();
let groupElements = document.querySelectorAll('[ml-server-group]');
groupElements.forEach(groupElement => {
let group = new Object();
group.name = groupElement.attributes.getNamedItem("ml-server-group").value;
let servers = new Array();
let serverElements = groupElement.querySelectorAll("[ml-server-id]");
serverElements.forEach(serverElement => {
let id = serverElement.attributes.getNamedItem("ml-server-id").value;
servers.push(id);
});
group.servers = servers;
groups.push(group);
});
return groups;
}
}
};

View File

@@ -67,6 +67,10 @@ Moonlight:
Daemon (not wings):
`curl https://install.moonlightpanel.xyz/daemon| bash`
Having any issues?
We are happy to help on our discord server:
[https://discord.gg/TJaspT7A8p](https://discord.gg/TJaspT7A8p)
## Roadmap
The roudmap can be found here:
@@ -96,3 +100,31 @@ Distributed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
* **Daniel Balk** - *Endelon Hosting* - [Daniel Balk](https://github.com/Daniel-Balk) - *Notification system & frontend*
* **Spielepapagei** - *Endelon Hosting* - [Spielepapagei](https://github.com/Spielepapagei) - *Discord Bot & support tickets*
* **Dannyx** - *None* - [Dannyx](https://github.com/Dannyx1604) - *Grammer check and translations*
## Some screenshots
Only user area
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635286443634768/dashboard.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635662475571261/serverlist_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635784685002762/console_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121635898933657741/filemanager_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636024162992128/filemanager_move_.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636204358672494/filemanager_editor.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636339285237820/backups.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636510182150215/addons.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636623784890519/settings.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636741170855967/webspace_overview.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636848670875668/webspace_files.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121636950953177138/webspace_databases.png)
![Screen Shot](https://cdn.discordapp.com/attachments/1059911407170228234/1121637134797918259/domains_.png)