diff --git a/Moonlight/App/Models/Forms/ServerOrderDataModel.cs b/Moonlight/App/Models/Forms/ServerOrderDataModel.cs new file mode 100644 index 00000000..778096f5 --- /dev/null +++ b/Moonlight/App/Models/Forms/ServerOrderDataModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Models.Forms; + +public class ServerOrderDataModel +{ + [Required(ErrorMessage = "You need to enter a name")] + [MaxLength(32, ErrorMessage = "The name cannot be longer that 32 characters")] + public string Name { get; set; } = ""; + + [Required(ErrorMessage = "You need to specify a server image")] + public Image Image { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Misc/SubscriptionLimit.cs b/Moonlight/App/Models/Misc/SubscriptionLimit.cs index 6e77abaf..21d59100 100644 --- a/Moonlight/App/Models/Misc/SubscriptionLimit.cs +++ b/Moonlight/App/Models/Misc/SubscriptionLimit.cs @@ -5,6 +5,12 @@ public class SubscriptionLimit public string Identifier { get; set; } = ""; public int Amount { get; set; } public List Options { get; set; } = new(); + + public string? ReadValue(string key) + { + var d = Options.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.InvariantCultureIgnoreCase)); + return d?.Value; + } public class LimitOption { diff --git a/Moonlight/App/Services/Interop/AlertService.cs b/Moonlight/App/Services/Interop/AlertService.cs index d03d4ef9..24a15fa4 100644 --- a/Moonlight/App/Services/Interop/AlertService.cs +++ b/Moonlight/App/Services/Interop/AlertService.cs @@ -5,10 +5,12 @@ namespace Moonlight.App.Services.Interop; public class AlertService { private readonly SweetAlertService SweetAlertService; + private readonly SmartTranslateService SmartTranslateService; - public AlertService(SweetAlertService service) + public AlertService(SweetAlertService service, SmartTranslateService smartTranslateService) { SweetAlertService = service; + SmartTranslateService = smartTranslateService; } public async Task Info(string title, string desciption) @@ -21,6 +23,11 @@ public class AlertService }); } + public async Task Info(string desciption) + { + await Info("", desciption); + } + public async Task Success(string title, string desciption) { await SweetAlertService.FireAsync(new SweetAlertOptions() @@ -31,6 +38,11 @@ public class AlertService }); } + public async Task Success(string desciption) + { + await Success("", desciption); + } + public async Task Warning(string title, string desciption) { await SweetAlertService.FireAsync(new SweetAlertOptions() @@ -41,6 +53,11 @@ public class AlertService }); } + public async Task Warning(string desciption) + { + await Warning("", desciption); + } + public async Task Error(string title, string desciption) { await SweetAlertService.FireAsync(new SweetAlertOptions() @@ -51,6 +68,11 @@ public class AlertService }); } + public async Task Error(string desciption) + { + await Error("", desciption); + } + public async Task YesNo(string title, string desciption, string yesText, string noText) { var result = await SweetAlertService.FireAsync(new SweetAlertOptions() @@ -79,4 +101,27 @@ public class AlertService return result.Value; } + + public async Task ConfirmMath() + { + var r = new Random(); + var i1 = r.Next(5, 15); + var i2 = r.Next(5, 15); + + var input = await Text( + SmartTranslateService.Translate("Confirm"), + $"{i1} + {i2} =", + "" + ); + + if (int.TryParse(input, out int i)) + { + if (i == i1 + i2) + { + return true; + } + } + + return false; + } } \ No newline at end of file diff --git a/Moonlight/App/Services/Interop/ClipboardService.cs b/Moonlight/App/Services/Interop/ClipboardService.cs index 79fa4202..a7c806c9 100644 --- a/Moonlight/App/Services/Interop/ClipboardService.cs +++ b/Moonlight/App/Services/Interop/ClipboardService.cs @@ -15,4 +15,9 @@ public class ClipboardService { await JsRuntime.InvokeVoidAsync("copyTextToClipboard", data); } + public async Task Copy(string data) + { + await JsRuntime.InvokeVoidAsync("copyTextToClipboard", data); + } + } \ No newline at end of file diff --git a/Moonlight/App/Services/SmartDeployService.cs b/Moonlight/App/Services/SmartDeployService.cs new file mode 100644 index 00000000..0585c1dd --- /dev/null +++ b/Moonlight/App/Services/SmartDeployService.cs @@ -0,0 +1,64 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services; + +public class SmartDeployService +{ + private readonly NodeRepository NodeRepository; + private readonly NodeService NodeService; + + public SmartDeployService(NodeRepository nodeRepository, NodeService nodeService) + { + NodeRepository = nodeRepository; + NodeService = nodeService; + } + + public async Task GetNode() + { + var data = new Dictionary(); + + foreach (var node in NodeRepository.Get().ToArray()) + { + var u = await GetUsageScore(node); + + if(u != 0) + data.Add(node, u); + } + + if (!data.Any()) + return null; + + return data.MaxBy(x => x.Value).Key; + } + + private async Task GetUsageScore(Node node) + { + var score = 0; + + try + { + var cpuStats = await NodeService.GetCpuStats(node); + var memoryStats = await NodeService.GetMemoryStats(node); + var diskStats = await NodeService.GetDiskStats(node); + + var cpuWeight = 0.5; // Weight of CPU usage in the final score + var memoryWeight = 0.3; // Weight of memory usage in the final score + var diskSpaceWeight = 0.2; // Weight of free disk space in the final score + + var cpuScore = (1 - cpuStats.Usage) * cpuWeight; // CPU score is based on the inverse of CPU usage + var memoryScore = (1 - (memoryStats.Used / 1024)) * memoryWeight; // Memory score is based on the percentage of free memory + var diskSpaceScore = (double) diskStats.FreeBytes / 1000000000 * diskSpaceWeight; // Disk space score is based on the amount of free disk space in GB + + var finalScore = cpuScore + memoryScore + diskSpaceScore; + + return finalScore; + } + catch (Exception e) + { + // ignored + } + + return score; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/SubscriptionService.cs b/Moonlight/App/Services/SubscriptionService.cs index f5e77c95..de23692a 100644 --- a/Moonlight/App/Services/SubscriptionService.cs +++ b/Moonlight/App/Services/SubscriptionService.cs @@ -78,49 +78,45 @@ public class SubscriptionService await OneTimeJwtService.Revoke(code); } + public async Task Cancel() + { + if (await GetCurrent() != null) + { + var user = await GetCurrentUser(); + + user.CurrentSubscription = null; + + UserRepository.Update(user); + } + } + public async Task GetLimit(string identifier) { - var configSection = ConfigService.GetSection("Moonlight").GetSection("Subscriptions"); - - var defaultLimits = configSection.GetValue("defaultLimits"); - var subscription = await GetCurrent(); if (subscription == null) { - var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier); - - if (foundDefault != null) - return foundDefault; - return new() { Identifier = identifier, Amount = 0 }; } - else + + var subscriptionLimits = + JsonConvert.DeserializeObject(subscription.LimitsJson) + ?? Array.Empty(); + + var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier); + + if (foundLimit != null) + return foundLimit; + + return new() { - var subscriptionLimits = - JsonConvert.DeserializeObject(subscription.LimitsJson) - ?? Array.Empty(); - - var foundLimit = subscriptionLimits.FirstOrDefault(x => x.Identifier == identifier); - - if (foundLimit != null) - return foundLimit; - - var foundDefault = defaultLimits.FirstOrDefault(x => x.Identifier == identifier); - - if (foundDefault != null) - return foundDefault; - - return new() - { - Identifier = identifier, - Amount = 0 - }; - } + Identifier = identifier, + Amount = 0 + }; } private async Task GetCurrentUser() diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index d7629715..cb4fdcf3 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -91,6 +91,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Moonlight/Shared/Layouts/MainLayout.razor b/Moonlight/Shared/Layouts/MainLayout.razor index 5345b49a..4c42ecb7 100644 --- a/Moonlight/Shared/Layouts/MainLayout.razor +++ b/Moonlight/Shared/Layouts/MainLayout.razor @@ -161,7 +161,8 @@ await JsRuntime.InvokeVoidAsync("document.body.removeAttribute", "data-kt-app-page-loading"); await JsRuntime.InvokeVoidAsync("KTMenu.createInstances"); await JsRuntime.InvokeVoidAsync("KTDrawer.createInstances"); - await JsRuntime.InvokeVoidAsync("createSnow"); + + //await JsRuntime.InvokeVoidAsync("createSnow"); await SessionService.Register(); diff --git a/Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor b/Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor new file mode 100644 index 00000000..87dd733c --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Subscriptions/Edit.razor @@ -0,0 +1,168 @@ +@page "/admin/subscriptions/edit/{Id:int}" +@using Moonlight.App.Models.Forms +@using Moonlight.App.Models.Misc +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Moonlight.App.Database.Entities + +@inject NavigationManager NavigationManager +@inject SubscriptionRepository SubscriptionRepository +@inject SubscriptionAdminService SubscriptionAdminService + + +
+ + @if (Subscription == null) + { +
+ No subscription with this id has been found +
+ } + else + { + + +
+ +
+ +
+ +
+ +
+ @foreach (var limitPart in Limits.Chunk(3)) + { +
+ @foreach (var limit in limitPart) + { +
+
+ +
+ +
+ +
+ +
+
+
+ Options +
+
+ +
+
+ + +
+
+
+
+ } +
+ } +
+ +
+ + +
+
+ } +
+
+
+ +@code +{ + [Parameter] + public int Id { get; set; } + + private Subscription? Subscription; + + private SubscriptionDataModel Model = new(); + private List Limits = new(); + + private async Task OnSubmit() + { + Subscription!.Name = Model.Name; + Subscription.Description = Model.Description; + + SubscriptionRepository.Update(Subscription); + + await SubscriptionAdminService.SaveLimits(Subscription, Limits.ToArray()); + + NavigationManager.NavigateTo("/admin/subscriptions"); + } + + private async Task Load(LazyLoader arg) + { + Subscription = SubscriptionRepository + .Get() + .FirstOrDefault(x => x.Id == Id); + + if (Subscription != null) + { + Model.Name = Subscription.Name; + Model.Description = Subscription.Description; + + Limits = (await SubscriptionAdminService.GetLimits(Subscription)).ToList(); + } + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor b/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor index 83c20654..e7182f6a 100644 --- a/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor +++ b/Moonlight/Shared/Views/Admin/Subscriptions/Index.razor @@ -3,10 +3,15 @@ @using Moonlight.App.Database.Entities @using Moonlight.App.Repositories @using BlazorTable +@using Moonlight.App.Services.Interop @inject SmartTranslateService SmartTranslateService @inject SubscriptionRepository SubscriptionRepository +@inject SubscriptionAdminService SubscriptionAdminService +@inject AlertService AlertService +@inject ClipboardService ClipboardService +
@@ -36,9 +41,16 @@ - + @@ -66,7 +78,24 @@ private async Task Delete(Subscription subscription) { SubscriptionRepository.Delete(subscription); - + await LazyLoader.Reload(); } + + private async Task GenerateCode(Subscription subscription) + { + var durationText = await AlertService.Text( + SmartTranslateService.Translate("Duration"), + SmartTranslateService.Translate("Enter duration of subscription"), + "30" + ); + + if (int.TryParse(durationText, out int duration)) + { + var code = await SubscriptionAdminService.GenerateCode(subscription, duration); + + await ClipboardService.Copy(code); + await AlertService.Success(SmartTranslateService.Translate("Copied code to clipboard")); + } + } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Profile/Subscriptions.razor b/Moonlight/Shared/Views/Profile/Subscriptions.razor new file mode 100644 index 00000000..c8382775 --- /dev/null +++ b/Moonlight/Shared/Views/Profile/Subscriptions.razor @@ -0,0 +1,112 @@ +@page "/profile/subscriptions" + +@using Moonlight.Shared.Components.Navigations +@using Moonlight.App.Services +@using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers +@using Moonlight.App.Services.Interop + +@inject ConfigService ConfigService +@inject AlertService AlertService +@inject SubscriptionService SubscriptionService +@inject SmartTranslateService SmartTranslateService + + + +
+
+
+ Subscription +
+
+
+ + @if (Subscription == null) + { + var config = ConfigService + .GetSection("Moonlight") + .GetSection("Subscriptions") + .GetSection("Sellpass"); + + var enableSellpass = config.GetValue("Enable"); + var url = config.GetValue("Url"); + +

+
+ + + +
+

+ + if (enableSellpass) + { + + } + } + else + { + var d = User.SubscriptionSince.AddDays(User.SubscriptionDuration).ToUniversalTime(); + +

+ Active until @(Formatter.FormatDateOnly(d)) +

+

+ Current subscription: @(Subscription.Name) +

+

+ @(Subscription.Description) +

+

+ We will send you a notification upon subscription expiration +

+
+ + +
+ } +
+
+
+
+
+ +@code +{ + [CascadingParameter] + public User User { get; set; } + + private Subscription? Subscription; + private LazyLoader LazyLoader; + + private string Code = ""; + + private async Task Load(LazyLoader arg) + { + Subscription = await SubscriptionService.GetCurrent(); + } + + private async Task Cancel() + { + if (await AlertService.ConfirmMath()) + { + await SubscriptionService.Cancel(); + await LazyLoader.Reload(); + } + } + + private async Task OnSubmit() + { + await SubscriptionService.ApplyCode(Code); + Code = ""; + await LazyLoader.Reload(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Servers/Create.razor b/Moonlight/Shared/Views/Servers/Create.razor new file mode 100644 index 00000000..85ec9d91 --- /dev/null +++ b/Moonlight/Shared/Views/Servers/Create.razor @@ -0,0 +1,226 @@ +@page "/servers/create" +@using Moonlight.App.Services +@using Moonlight.App.Database.Entities +@using Moonlight.App.Models.Forms +@using Moonlight.App.Models.Misc +@using Moonlight.App.Repositories +@using Moonlight.App.Repositories.Servers +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Exceptions + +@inject SubscriptionService SubscriptionService +@inject ImageRepository ImageRepository +@inject SmartTranslateService SmartTranslateService +@inject SmartDeployService SmartDeployService +@inject ServerRepository ServerRepository +@inject NavigationManager NavigationManager +@inject ServerService ServerService + + + @if (DeployNode == null) + { +
+
+ Not found image +
+

+ No node found +

+

+ No node found to deploy to found +

+
+
+
+ } + else + { +
+
+
+
+
+

+ Server details +

+
+
+
+
+
+ +
@(DeployNode.Name)
+
+ @if (Model.Image != null) + { + var limit = Images[Model.Image]; + +
+ +
@(Model.Image.Name)
+
+ +
+ +
+ @{ + var cpu = limit.ReadValue("cpu"); + + if (cpu == null) + cpu = "N/A"; + else + cpu = (int.Parse(cpu) / 100).ToString(); + } + @(cpu) Cores +
+
+ +
+ +
@(limit.ReadValue("memory")) MB
+
+ +
+ +
@(limit.ReadValue("disk")) MB
+
+ } +
+
+
+
+
+
+
+
+

+ Configure your server +

+
+
+
+ + +
+ +
+ @if (Images.Any()) + { + + + + + + } + else + { +
+ + You reached the maximum amount of servers for every image of your subscription: @(Subscription == null ? SmartTranslateService.Translate("Default") : Subscription.Name) + +
+ } +
+
+
+
+
+ } +
+ +@code +{ + [CascadingParameter] + public User User { get; set; } + + private Node? DeployNode; + private Subscription? Subscription; + + private Dictionary Images = new(); + + private ServerOrderDataModel Model = new(); + + private async Task Load(LazyLoader lazyLoader) + { + // Reset state + Images.Clear(); + Model = new(); + + await lazyLoader.SetText(SmartTranslateService.Translate("Loading your subscription")); + Subscription = await SubscriptionService.GetCurrent(); + + await lazyLoader.SetText(SmartTranslateService.Translate("Searching for deploy node")); + + DeployNode = await SmartDeployService.GetNode(); + + await lazyLoader.SetText(SmartTranslateService.Translate("Searching for available images")); + + var images = ImageRepository.Get().ToArray(); + + foreach (var image in images) + { + var limit = await SubscriptionService.GetLimit("image." + image.Id); + + if (limit.Amount > 0) + { + var serversCount = ServerRepository + .Get() + .Include(x => x.Owner) + .Include(x => x.Image) + .Where(x => x.Owner.Id == User.Id) + .Count(x => x.Image.Id == image.Id); + + if(serversCount < limit.Amount) + Images.Add(image, limit); + } + } + } + + private async Task OnValidSubmit() + { + var limit = await SubscriptionService.GetLimit("image." + Model.Image.Id); + + if (limit.Amount > 0) + { + var serversCount = ServerRepository + .Get() + .Include(x => x.Owner) + .Include(x => x.Image) + .Where(x => x.Owner.Id == User.Id) + .Count(x => x.Image.Id == Model.Image.Id); + + if (serversCount < limit.Amount) + { + if(int.TryParse(limit.ReadValue("cpu"), out int cpu) && + int.TryParse(limit.ReadValue("memory"), out int memory) && + int.TryParse(limit.ReadValue("disk"), out int disk)) + { + var server = await ServerService.Create( + Model.Name, + cpu, + memory, + disk, + User, + Model.Image, + DeployNode + ); + + NavigationManager.NavigateTo($"/server/{server.Uuid}"); + } + else + { + throw new DisplayException("Limits cannot be parsed"); + } + } + } + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Servers.razor b/Moonlight/Shared/Views/Servers/Index.razor similarity index 100% rename from Moonlight/Shared/Views/Servers.razor rename to Moonlight/Shared/Views/Servers/Index.razor diff --git a/Moonlight/resources/lang/de_de.lang b/Moonlight/resources/lang/de_de.lang index 02ca6ef0..c60df72b 100644 --- a/Moonlight/resources/lang/de_de.lang +++ b/Moonlight/resources/lang/de_de.lang @@ -461,9 +461,4 @@ Add new limit;Add new limit Create subscription;Create subscription Options;Options Amount;Amount -Do you really want to delete it?;Do you really want to delete it? -Change your password;Change your password -You need to change your password in order to use moonlight;You need to change your password in order to use moonlight -You need to enter your full name in order to use moonlight;You need to enter your full name in order to use moonlight -Enter your information;Enter your information -The field FirstName must be a string or array type with a minimum length of '2'.;The field FirstName must be a string or array type with a minimum length of '2'. +Do you really want to delete it?;Do you really want to delete it? \ No newline at end of file diff --git a/Moonlight/wwwroot/assets/media/svg/subscription.svg b/Moonlight/wwwroot/assets/media/svg/subscription.svg new file mode 100644 index 00000000..6022690d --- /dev/null +++ b/Moonlight/wwwroot/assets/media/svg/subscription.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file